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. limitations under the License.
*/ */
import React, { ReactElement, ReactNode } from "react"; import React, { LegacyRef, ReactElement, ReactNode } from "react";
import sanitizeHtml from "sanitize-html"; import sanitizeHtml from "sanitize-html";
import cheerio from "cheerio"; import cheerio from "cheerio";
import classNames from "classnames"; 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 * positives, but useful for fast-path testing strings to see if they
* need emojification. * need emojification.
*/ */
function mightContainEmoji(str: string): boolean { function mightContainEmoji(str?: string): boolean {
return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str); 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 * @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 * 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 emojiToSpan = isHtmlMessage ? emojiToHtmlSpan : emojiToJsxSpan;
const result: (JSX.Element | string)[] = []; const result: (JSX.Element | string)[] = [];
let text = ""; let text = "";
@ -641,9 +641,9 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
* @return The HTML-ified node. * @return The HTML-ified node.
*/ */
export function topicToHtml( export function topicToHtml(
topic: string, topic?: string,
htmlTopic?: string, htmlTopic?: string,
ref?: React.Ref<HTMLSpanElement>, ref?: LegacyRef<HTMLSpanElement>,
allowExtendedHtml = false, allowExtendedHtml = false,
): ReactNode { ): ReactNode {
if (!SettingsStore.getValue("feature_html_topic")) { 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. limitations under the License.
*/ */
import React from "react"; import React, { ReactElement } from "react";
import { COUNTRIES, getEmojiFlag, PhoneNumberCountryDefinition } from "../../../phonenumber"; import { COUNTRIES, getEmojiFlag, PhoneNumberCountryDefinition } from "../../../phonenumber";
import SdkConfig from "../../../SdkConfig"; import SdkConfig from "../../../SdkConfig";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import Dropdown from "../elements/Dropdown"; import Dropdown from "../elements/Dropdown";
import { NonEmptyArray } from "../../../@types/common";
const COUNTRIES_BY_ISO2: Record<string, PhoneNumberCountryDefinition> = {}; const COUNTRIES_BY_ISO2: Record<string, PhoneNumberCountryDefinition> = {};
for (const c of COUNTRIES) { for (const c of COUNTRIES) {
@ -131,7 +132,7 @@ export default class CountryDropdown extends React.Component<IProps, IState> {
{_t(country.name)} (+{country.prefix}) {_t(country.name)} (+{country.prefix})
</div> </div>
); );
}); }) as NonEmptyArray<ReactElement & { key: string }>;
// default value here too, otherwise we need to handle null / undefined // default value here too, otherwise we need to handle null / undefined
// values between mounting and the initial value propagating // 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. 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 classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { sleep } from "matrix-js-sdk/src/utils"; import { sleep } from "matrix-js-sdk/src/utils";
@ -41,6 +41,7 @@ import QueryMatcher from "../../../autocomplete/QueryMatcher";
import LazyRenderList from "../elements/LazyRenderList"; import LazyRenderList from "../elements/LazyRenderList";
import { useSettingValue } from "../../../hooks/useSettings"; import { useSettingValue } from "../../../hooks/useSettings";
import { filterBoolean } from "../../../utils/arrays"; import { filterBoolean } from "../../../utils/arrays";
import { NonEmptyArray } from "../../../@types/common";
// These values match CSS // These values match CSS
const ROW_HEIGHT = 32 + 12; const ROW_HEIGHT = 32 + 12;
@ -415,7 +416,8 @@ export const SubspaceSelector: React.FC<ISubspaceSelectorProps> = ({ title, spac
value={value.roomId} value={value.roomId}
label={_t("Space selection")} label={_t("Space selection")}
> >
{options.map((space) => { {
options.map((space) => {
const classes = classNames({ const classes = classNames({
mx_SubspaceSelector_dropdownOptionActive: space === value, mx_SubspaceSelector_dropdownOptionActive: space === value,
}); });
@ -425,7 +427,8 @@ export const SubspaceSelector: React.FC<ISubspaceSelectorProps> = ({ title, spac
{space.name || getDisplayAliasForRoom(space) || space.roomId} {space.name || getDisplayAliasForRoom(space) || space.roomId}
</div> </div>
); );
})} }) as NonEmptyArray<ReactElement & { key: string }>
}
</Dropdown> </Dropdown>
); );
} else { } else {

View file

@ -40,7 +40,7 @@ interface IProps {
interface IState { interface IState {
roomMember: RoomMember; roomMember: RoomMember;
isWrapped: boolean; isWrapped: boolean;
widgetDomain: string; widgetDomain: string | null;
} }
export default class AppPermission extends React.Component<IProps, IState> { 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 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`. // HACK: We're relying on the query params when we should be relying on the widget's `data`.
// This is a workaround for Scalar. // This is a workaround for Scalar.
if (WidgetUtils.isScalarUrl(this.props.url) && params && params.get("url")) { if (WidgetUtils.isScalarUrl(this.props.url) && params?.get("url")) {
const unwrappedUrl = url.parse(params.get("url")); const unwrappedUrl = url.parse(params.get("url")!);
return { return {
widgetDomain: unwrappedUrl.host || unwrappedUrl.hostname, widgetDomain: unwrappedUrl.host || unwrappedUrl.hostname,
isWrapped: true, isWrapped: true,

View file

@ -586,7 +586,7 @@ export default class AppTile extends React.Component<IProps, IState> {
<AppWarning errorMsg={_t("Error loading Widget")} /> <AppWarning errorMsg={_t("Error loading Widget")} />
</div> </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 // only possible for room widgets, can assert this.props.room here
const isEncrypted = this.context.isRoomEncrypted(this.props.room.roomId); const isEncrypted = this.context.isRoomEncrypted(this.props.room.roomId);
appTileBody = ( appTileBody = (
@ -689,11 +689,9 @@ export default class AppTile extends React.Component<IProps, IState> {
const layoutButtons: ReactNode[] = []; const layoutButtons: ReactNode[] = [];
if (this.props.showLayoutButtons) { if (this.props.showLayoutButtons) {
const isMaximised = WidgetLayoutStore.instance.isInContainer( const isMaximised =
this.props.room, this.props.room &&
this.props.app, WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, Container.Center);
Container.Center,
);
const maximisedClasses = classNames({ const maximisedClasses = classNames({
mx_AppTileMenuBar_iconButton: true, mx_AppTileMenuBar_iconButton: true,
mx_AppTileMenuBar_iconButton_collapse: isMaximised, mx_AppTileMenuBar_iconButton_collapse: isMaximised,

View file

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

View file

@ -23,6 +23,7 @@ import { _t } from "../../../languageHandler";
import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { objectHasDiff } from "../../../utils/objects"; import { objectHasDiff } from "../../../utils/objects";
import { NonEmptyArray } from "../../../@types/common";
interface IMenuOptionProps { interface IMenuOptionProps {
children: ReactElement; children: ReactElement;
@ -77,7 +78,7 @@ export interface DropdownProps {
label: string; label: string;
value?: string; value?: string;
className?: string; className?: string;
children: ReactElement[]; children: NonEmptyArray<ReactElement & { key: string }>;
// negative for consistency with HTML // negative for consistency with HTML
disabled?: boolean; disabled?: boolean;
// The width that the dropdown should be. If specified, // The width that the dropdown should be. If specified,
@ -102,7 +103,7 @@ export interface DropdownProps {
interface IState { interface IState {
expanded: boolean; expanded: boolean;
highlightedOption: string | null; highlightedOption: string;
searchQuery: string; searchQuery: string;
} }
@ -122,14 +123,14 @@ export default class Dropdown extends React.Component<DropdownProps, IState> {
this.reindexChildren(this.props.children); this.reindexChildren(this.props.children);
const firstChild = React.Children.toArray(props.children)[0] as ReactElement; const firstChild = props.children[0];
this.state = { this.state = {
// True if the menu is dropped-down // True if the menu is dropped-down
expanded: false, expanded: false,
// The key of the highlighted option // The key of the highlighted option
// (the option that would become selected if you pressed enter) // (the option that would become selected if you pressed enter)
highlightedOption: firstChild ? (firstChild.key as string) : null, highlightedOption: firstChild.key,
// the current search query // the current search query
searchQuery: "", searchQuery: "",
}; };
@ -144,7 +145,7 @@ export default class Dropdown extends React.Component<DropdownProps, IState> {
this.reindexChildren(this.props.children); this.reindexChildren(this.props.children);
const firstChild = this.props.children[0]; const firstChild = this.props.children[0];
this.setState({ 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 { private reindexChildren(children: ReactElement[]): void {
this.childrenByKey = {}; this.childrenByKey = {};
React.Children.forEach(children, (child) => { React.Children.forEach(children, (child) => {
this.childrenByKey[child.key] = child; this.childrenByKey[(child as DropdownProps["children"][number]).key] = child;
}); });
} }
@ -291,14 +292,12 @@ export default class Dropdown extends React.Component<DropdownProps, IState> {
return keys[index <= 0 ? keys.length - 1 : (index - 1) % keys.length]; return keys[index <= 0 ? keys.length - 1 : (index - 1) % keys.length];
} }
private scrollIntoView(node: Element): void { private scrollIntoView(node: Element | null): void {
if (node) { node?.scrollIntoView({
node.scrollIntoView({
block: "nearest", block: "nearest",
behavior: "auto", behavior: "auto",
}); });
} }
}
private getMenuOptions(): JSX.Element[] { private getMenuOptions(): JSX.Element[] {
const options = React.Children.map(this.props.children, (child: ReactElement) => { const options = React.Children.map(this.props.children, (child: ReactElement) => {
@ -317,7 +316,7 @@ export default class Dropdown extends React.Component<DropdownProps, IState> {
</MenuOption> </MenuOption>
); );
}); });
if (options.length === 0) { if (!options?.length) {
return [ return [
<div key="0" className="mx_Dropdown_option" role="option" aria-selected={false}> <div key="0" className="mx_Dropdown_option" role="option" aria-selected={false}>
{_t("No results")} {_t("No results")}
@ -363,9 +362,13 @@ export default class Dropdown extends React.Component<DropdownProps, IState> {
} }
if (!currentValue) { if (!currentValue) {
const selectedChild = this.props.getShortOption let selectedChild: ReactNode | undefined;
if (this.props.value) {
selectedChild = this.props.getShortOption
? this.props.getShortOption(this.props.value) ? this.props.getShortOption(this.props.value)
: this.childrenByKey[this.props.value]; : this.childrenByKey[this.props.value];
}
currentValue = ( currentValue = (
<div className="mx_Dropdown_option" id={`${this.props.id}_value`}> <div className="mx_Dropdown_option" id={`${this.props.id}_value`}>
{selectedChild || this.props.placeholder} {selectedChild || this.props.placeholder}

View file

@ -87,6 +87,7 @@ export default class EditableText extends React.Component<IProps, IState> {
} }
private showPlaceholder = (show: boolean): void => { private showPlaceholder = (show: boolean): void => {
if (!this.editableDiv.current) return;
if (show) { if (show) {
this.editableDiv.current.textContent = this.props.placeholder; this.editableDiv.current.textContent = this.props.placeholder;
this.editableDiv.current.setAttribute( this.editableDiv.current.setAttribute(
@ -134,7 +135,7 @@ export default class EditableText extends React.Component<IProps, IState> {
if (!(ev.target as HTMLDivElement).textContent) { if (!(ev.target as HTMLDivElement).textContent) {
this.showPlaceholder(true); this.showPlaceholder(true);
} else if (!this.placeholder) { } else if (!this.placeholder) {
this.value = (ev.target as HTMLDivElement).textContent; this.value = (ev.target as HTMLDivElement).textContent ?? "";
} }
const action = getKeyBindingsManager().getAccessibilityAction(ev); const action = getKeyBindingsManager().getAccessibilityAction(ev);
@ -163,7 +164,7 @@ export default class EditableText extends React.Component<IProps, IState> {
range.setStart(node, 0); range.setStart(node, 0);
range.setEnd(node, ev.target.childNodes.length); range.setEnd(node, ev.target.childNodes.length);
const sel = window.getSelection(); const sel = window.getSelection()!;
sel.removeAllRanges(); sel.removeAllRanges();
sel.addRange(range); sel.addRange(range);
} }
@ -190,7 +191,7 @@ export default class EditableText extends React.Component<IProps, IState> {
}; };
private onBlur = (ev: React.FocusEvent<HTMLDivElement>): void => { private onBlur = (ev: React.FocusEvent<HTMLDivElement>): void => {
const sel = window.getSelection(); const sel = window.getSelection()!;
sel.removeAllRanges(); sel.removeAllRanges();
if (this.props.blurToCancel) { if (this.props.blurToCancel) {

View file

@ -54,14 +54,14 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
}; };
const onAction = (payload: { action: string }): void => { const onAction = (payload: { action: string }): void => {
const actionPrefix = "effects."; const actionPrefix = "effects.";
if (payload.action.indexOf(actionPrefix) === 0) { if (canvasRef.current && payload.action.startsWith(actionPrefix)) {
const effect = payload.action.slice(actionPrefix.length); 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 dispatcherRef = dis.register(onAction);
const canvas = canvasRef.current; const canvas = canvasRef.current;
canvas.height = UIStore.instance.windowHeight; if (canvas) canvas.height = UIStore.instance.windowHeight;
UIStore.instance.on(UI_EVENTS.Resize, resize); UIStore.instance.on(UI_EVENTS.Resize, resize);
return () => { return () => {

View file

@ -78,7 +78,9 @@ enum TransitionType {
const SEP = ","; 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 static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>; public context!: React.ContextType<typeof RoomContext>;
@ -508,12 +510,12 @@ export default class EventListSummary extends React.Component<IProps> {
const type = e.getType(); const type = e.getType();
let userKey = e.getSender()!; let userKey = e.getSender()!;
if (type === EventType.RoomThirdPartyInvite) { if (e.isState() && type === EventType.RoomThirdPartyInvite) {
userKey = e.getContent().display_name; userKey = e.getContent().display_name;
} else if (type === EventType.RoomMember) { } else if (e.isState() && type === EventType.RoomMember) {
userKey = e.getStateKey(); userKey = e.getStateKey()!;
} else if (e.isRedacted()) { } else if (e.isRedacted() && e.getUnsigned()?.redacted_because) {
userKey = e.getUnsigned()?.redacted_because?.sender; userKey = e.getUnsigned().redacted_because!.sender;
} }
// Initialise a user's events // Initialise a user's events

View file

@ -38,8 +38,6 @@ export interface IValidateOpts {
interface IProps { interface IProps {
// The field's ID, which binds the input and label together. Immutable. // The field's ID, which binds the input and label together. Immutable.
id?: string; id?: string;
// id of a <datalist> element for suggestions
list?: string;
// The field's label string. // The field's label string.
label?: string; label?: string;
// The field's placeholder string. Defaults to the label. // The field's placeholder string. Defaults to the label.
@ -119,7 +117,7 @@ interface IState {
} }
export default class Field extends React.PureComponent<PropShapes, IState> { export default class Field extends React.PureComponent<PropShapes, IState> {
private id: string; private readonly id: string;
private inputRef: RefObject<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>; private inputRef: RefObject<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>;
public static readonly defaultProps = { public static readonly defaultProps = {
@ -243,7 +241,6 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
tooltipContent, tooltipContent,
forceValidity, forceValidity,
tooltipClassName, tooltipClassName,
list,
validateOnBlur, validateOnBlur,
validateOnChange, validateOnChange,
validateOnFocus, validateOnFocus,
@ -262,7 +259,11 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
inputProps.onBlur = this.onBlur; inputProps.onBlur = this.onBlur;
// Appease typescript's inference // 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); 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 // Handle displaying feedback on validity
let fieldTooltip; let fieldTooltip: JSX.Element | undefined;
if (tooltipContent || this.state.feedback) { if (tooltipContent || this.state.feedback) {
let role: React.AriaRole; let role: React.AriaRole;
if (tooltipContent) { if (tooltipContent) {

View file

@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from "react"; import React, { ReactElement } from "react";
import classNames from "classnames"; import classNames from "classnames";
import { Icon as CheckmarkIcon } from "../../../../res/img/element-icons/roomlist/checkmark.svg"; import { Icon as CheckmarkIcon } from "../../../../res/img/element-icons/roomlist/checkmark.svg";
import Dropdown, { DropdownProps } from "./Dropdown"; import Dropdown, { DropdownProps } from "./Dropdown";
import { NonEmptyArray } from "../../../@types/common";
export type FilterDropdownOption<FilterKeysType extends string> = { export type FilterDropdownOption<FilterKeysType extends string> = {
id: FilterKeysType; id: FilterKeysType;
@ -63,13 +64,15 @@ export const FilterDropdown = <FilterKeysType extends string = string>({
className={classNames("mx_FilterDropdown", className)} className={classNames("mx_FilterDropdown", className)}
getShortOption={getSelectedFilterOptionComponent<FilterKeysType>(options, selectedLabel)} getShortOption={getSelectedFilterOptionComponent<FilterKeysType>(options, selectedLabel)}
> >
{options.map(({ id, label, description }) => ( {
options.map(({ id, label, description }) => (
<div className="mx_FilterDropdown_option" data-testid={`filter-option-${id}`} key={id}> <div className="mx_FilterDropdown_option" data-testid={`filter-option-${id}`} key={id}>
{id === value && <CheckmarkIcon className="mx_FilterDropdown_optionSelectedIcon" />} {id === value && <CheckmarkIcon className="mx_FilterDropdown_optionSelectedIcon" />}
<span className="mx_FilterDropdown_optionLabel">{label}</span> <span className="mx_FilterDropdown_optionLabel">{label}</span>
{!!description && <span className="mx_FilterDropdown_optionDescription">{description}</span>} {!!description && <span className="mx_FilterDropdown_optionDescription">{description}</span>}
</div> </div>
))} )) as NonEmptyArray<ReactElement & { key: string }>
}
</Dropdown> </Dropdown>
); );
}; };

View file

@ -96,17 +96,24 @@ export default class ImageView extends React.Component<IProps, IState> {
const { thumbnailInfo } = this.props; 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 = { this.state = {
zoom: 0, // We default to 0 and override this in imageLoaded once we have naturalSize zoom: 0, // We default to 0 and override this in imageLoaded once we have naturalSize
minZoom: MAX_SCALE, minZoom: MAX_SCALE,
maxZoom: MAX_SCALE, maxZoom: MAX_SCALE,
rotation: 0, rotation: 0,
translationX: thumbnailInfo?.positionX + thumbnailInfo?.width / 2 - UIStore.instance.windowWidth / 2 ?? 0, translationX,
translationY: translationY,
thumbnailInfo?.positionY +
thumbnailInfo?.height / 2 -
UIStore.instance.windowHeight / 2 -
getPanelHeight() / 2 ?? 0,
moving: false, moving: false,
contextMenuDisplayed: false, contextMenuDisplayed: false,
}; };
@ -143,6 +150,7 @@ export default class ImageView extends React.Component<IProps, IState> {
} }
private imageLoaded = (): void => { private imageLoaded = (): void => {
if (!this.image.current) return;
// First, we calculate the zoom, so that the image has the same size as // First, we calculate the zoom, so that the image has the same size as
// the thumbnail // the thumbnail
const { thumbnailInfo } = this.props; const { thumbnailInfo } = this.props;
@ -226,22 +234,23 @@ export default class ImageView extends React.Component<IProps, IState> {
translationX: 0, translationX: 0,
translationY: 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 // Zoom relative to the center of the view
this.setState({ this.setState({
zoom: newZoom, zoom: newZoom,
translationX: (this.state.translationX * newZoom) / oldZoom, translationX: (this.state.translationX * newZoom) / oldZoom,
translationY: (this.state.translationY * newZoom) / oldZoom, translationY: (this.state.translationY * newZoom) / oldZoom,
}); });
} else { } else if (this.image.current) {
// Zoom relative to the given point on the image. // Zoom relative to the given point on the image.
// First we need to figure out the offset of the anchor point // First we need to figure out the offset of the anchor point
// relative to the center of the image, accounting for rotation. // relative to the center of the image, accounting for rotation.
let offsetX: number | undefined; let offsetX: number;
let offsetY: number | undefined; let offsetY: number;
// The modulo operator can return negative values for some // The modulo operator can return negative values for some
// rotations, so we have to do some extra work to normalize it // 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: case 0:
offsetX = this.image.current.clientWidth / 2 - anchorX; offsetX = this.image.current.clientWidth / 2 - anchorX;
offsetY = this.image.current.clientHeight / 2 - anchorY; offsetY = this.image.current.clientHeight / 2 - anchorY;
@ -384,7 +393,7 @@ export default class ImageView extends React.Component<IProps, IState> {
private onEndMoving = (): void => { private onEndMoving = (): void => {
// Zoom out if we haven't moved much // Zoom out if we haven't moved much
if ( if (
this.state.moving === true && this.state.moving &&
Math.abs(this.state.translationX - this.previousX) < ZOOM_DISTANCE && Math.abs(this.state.translationX - this.previousX) < ZOOM_DISTANCE &&
Math.abs(this.state.translationY - this.previousY) < 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 { private renderContextMenu(): JSX.Element {
let contextMenu: JSX.Element | undefined; let contextMenu: JSX.Element | undefined;
if (this.state.contextMenuDisplayed) { if (this.state.contextMenuDisplayed && this.props.mxEvent) {
contextMenu = ( contextMenu = (
<MessageContextMenu <MessageContextMenu
{...aboveLeftOf(this.contextMenuButton.current.getBoundingClientRect())} {...aboveLeftOf(this.contextMenuButton.current.getBoundingClientRect())}
@ -445,7 +454,7 @@ export default class ImageView extends React.Component<IProps, IState> {
const showTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps"); const showTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
let permalink = "#"; let permalink = "#";
if (this.props.permalinkCreator) { 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(); 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 => { private onMouseMove = (ev: MouseEvent): void => {
const { clientX: x, clientY: y } = ev; const { clientX: x, clientY: y } = ev;
const { contentRect } = this.state; const { contentRect } = this.state;
if (!contentRect) return;
const targetRect = this.target.getBoundingClientRect(); const targetRect = this.target.getBoundingClientRect();
let direction: Direction; let direction: Direction;

View file

@ -14,10 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from "react"; import React, { ReactElement } from "react";
import { JoinRule } from "matrix-js-sdk/src/@types/partials"; import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import Dropdown from "./Dropdown"; import Dropdown from "./Dropdown";
import { NonEmptyArray } from "../../../@types/common";
interface IProps { interface IProps {
value: JoinRule; value: JoinRule;
@ -45,13 +46,15 @@ const JoinRuleDropdown: React.FC<IProps> = ({
<div key={JoinRule.Public} className="mx_JoinRuleDropdown_public"> <div key={JoinRule.Public} className="mx_JoinRuleDropdown_public">
{labelPublic} {labelPublic}
</div>, </div>,
]; ] as NonEmptyArray<ReactElement & { key: string }>;
if (labelRestricted) { if (labelRestricted) {
options.unshift( options.unshift(
(
<div key={JoinRule.Restricted} className="mx_JoinRuleDropdown_restricted"> <div key={JoinRule.Restricted} className="mx_JoinRuleDropdown_restricted">
{labelRestricted} {labelRestricted}
</div>, </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. limitations under the License.
*/ */
import React from "react"; import React, { ReactElement } from "react";
import * as languageHandler from "../../../languageHandler"; import * as languageHandler from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import Spinner from "./Spinner"; import Spinner from "./Spinner";
import Dropdown from "./Dropdown"; import Dropdown from "./Dropdown";
import { NonEmptyArray } from "../../../@types/common";
type Languages = Awaited<ReturnType<typeof languageHandler.getAllLanguagesFromJson>>; 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) => { const options = displayedLanguages.map((language) => {
return <div key={language.value}>{language.label}</div>; return <div key={language.value}>{language.label}</div>;
}); }) as NonEmptyArray<ReactElement & { key: string }>;
// default value here too, otherwise we need to handle null / undefined // default value here too, otherwise we need to handle null / undefined
// values between mounting and the initial value propagating // values between mounting and the initial value propagating

View file

@ -16,7 +16,7 @@ limitations under the License.
import classNames from "classnames"; import classNames from "classnames";
import { EventType } from "matrix-js-sdk/src/@types/event"; 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 MatrixClientContext from "../../../contexts/MatrixClientContext";
import RoomContext from "../../../contexts/RoomContext"; import RoomContext from "../../../contexts/RoomContext";
@ -59,7 +59,7 @@ const MiniAvatarUploader: React.FC<IProps> = ({
setShow(false); setShow(false);
}, 13000); // hide after being shown for 10 seconds }, 13000); // hide after being shown for 10 seconds
const uploadRef = useRef<HTMLInputElement>(); const uploadRef = useRef() as RefObject<HTMLInputElement>;
const label = hasAvatar || busy ? hasAvatarLabel : noAvatarLabel; const label = hasAvatar || busy ? hasAvatarLabel : noAvatarLabel;
@ -97,7 +97,7 @@ const MiniAvatarUploader: React.FC<IProps> = ({
})} })}
disabled={busy} disabled={busy}
onClick={() => { onClick={() => {
uploadRef.current.click(); uploadRef.current?.click();
}} }}
onMouseOver={() => setHover(true)} onMouseOver={() => setHover(true)}
onMouseLeave={() => setHover(false)} onMouseLeave={() => setHover(false)}

View file

@ -31,7 +31,7 @@ import { Action } from "../../../dispatcher/actions";
import Spinner from "./Spinner"; import Spinner from "./Spinner";
import ReplyTile from "../rooms/ReplyTile"; import ReplyTile from "../rooms/ReplyTile";
import { Pill, PillType } from "./Pill"; import { Pill, PillType } from "./Pill";
import AccessibleButton, { ButtonEvent } from "./AccessibleButton"; import AccessibleButton from "./AccessibleButton";
import { getParentEventId, shouldDisplayReply } from "../../../utils/Reply"; import { getParentEventId, shouldDisplayReply } from "../../../utils/Reply";
import RoomContext from "../../../contexts/RoomContext"; import RoomContext from "../../../contexts/RoomContext";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
@ -45,7 +45,7 @@ const SHOW_EXPAND_QUOTE_PIXELS = 60;
interface IProps { interface IProps {
// the latest event in this chain of replies // the latest event in this chain of replies
parentEv?: MatrixEvent; parentEv: MatrixEvent;
// called when the ReplyChain contents has changed, including EventTiles thereof // called when the ReplyChain contents has changed, including EventTiles thereof
onHeightChanged: () => void; onHeightChanged: () => void;
permalinkCreator?: RoomPermalinkCreator; permalinkCreator?: RoomPermalinkCreator;
@ -91,7 +91,7 @@ export default class ReplyChain extends React.Component<IProps, IState> {
err: false, err: false,
}; };
this.room = this.matrixClient.getRoom(this.props.parentEv.getRoomId()); this.room = this.matrixClient.getRoom(this.props.parentEv.getRoomId())!;
} }
private get matrixClient(): MatrixClient { 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; if (!eventId) return null;
const event = this.room.findEventById(eventId); const event = this.room.findEventById(eventId);
if (event) return event; if (event) return event;
@ -180,7 +180,8 @@ export default class ReplyChain extends React.Component<IProps, IState> {
this.initialize(); 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]; const events = [this.state.loadedEv, ...this.state.events];
let loadedEv: MatrixEvent | null = null; 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. 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 { Room } from "matrix-js-sdk/src/models/room";
import classNames from "classnames"; import classNames from "classnames";
import { EventType } from "matrix-js-sdk/src/@types/event"; 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 { export default function RoomTopic({ room, ...props }: IProps): JSX.Element {
const client = useContext(MatrixClientContext); const client = useContext(MatrixClientContext);
const ref = useRef<HTMLDivElement>(); const ref = useRef() as RefObject<HTMLDivElement>;
const topic = useTopic(room); const topic = useTopic(room);
const body = topicToHtml(topic?.text, topic?.html, ref); 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. limitations under the License.
*/ */
import React from "react"; import React, { ReactElement } from "react";
import Dropdown from "../../views/elements/Dropdown"; import Dropdown from "../../views/elements/Dropdown";
import PlatformPeg from "../../../PlatformPeg"; import PlatformPeg from "../../../PlatformPeg";
@ -22,6 +22,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import Spinner from "./Spinner"; import Spinner from "./Spinner";
import * as languageHandler from "../../../languageHandler"; import * as languageHandler from "../../../languageHandler";
import { NonEmptyArray } from "../../../@types/common";
type Languages = Awaited<ReturnType<typeof languageHandler.getAllLanguagesFromJson>>; type Languages = Awaited<ReturnType<typeof languageHandler.getAllLanguagesFromJson>>;
function languageMatchesSearchQuery(query: string, language: Languages[0]): boolean { function languageMatchesSearchQuery(query: string, language: Languages[0]): boolean {
@ -106,7 +107,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component<
const options = displayedLanguages.map((language) => { const options = displayedLanguages.map((language) => {
return <div key={language.value}>{language.label}</div>; return <div key={language.value}>{language.label}</div>;
}); }) as NonEmptyArray<ReactElement & { key: string }>;
// default value here too, otherwise we need to handle null / undefined; // default value here too, otherwise we need to handle null / undefined;
// values between mounting and the initial value propagating // 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. limitations under the License.
*/ */
import React from "react"; import React, { ReactElement } from "react";
import { formatDuration } from "../../../DateUtils"; import { formatDuration } from "../../../DateUtils";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import Dropdown from "../elements/Dropdown"; import Dropdown from "../elements/Dropdown";
import { NonEmptyArray } from "../../../@types/common";
const DURATION_MS = { const DURATION_MS = {
fifteenMins: 900000, fifteenMins: 900000,
@ -68,11 +69,13 @@ const LiveDurationDropdown: React.FC<Props> = ({ timeout, onChange }) => {
onOptionChange={onOptionChange} onOptionChange={onOptionChange}
className="mx_LiveDurationDropdown" className="mx_LiveDurationDropdown"
> >
{options.map(({ key, label }) => ( {
options.map(({ key, label }) => (
<div data-test-id={`live-duration-option-${key}`} key={key}> <div data-test-id={`live-duration-option-${key}`} key={key}>
{label} {label}
</div> </div>
))} )) as NonEmptyArray<ReactElement & { key: string }>
}
</Dropdown> </Dropdown>
); );
}; };

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { useMemo } from "react"; import React, { ReactElement, useMemo } from "react";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
@ -26,6 +26,7 @@ import { SettingLevel } from "../../../settings/SettingLevel";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import { RecheckThemePayload } from "../../../dispatcher/payloads/RecheckThemePayload"; import { RecheckThemePayload } from "../../../dispatcher/payloads/RecheckThemePayload";
import PosthogTrackers from "../../../PosthogTrackers"; import PosthogTrackers from "../../../PosthogTrackers";
import { NonEmptyArray } from "../../../@types/common";
type Props = { type Props = {
requestClose: () => void; requestClose: () => void;
@ -86,9 +87,11 @@ const QuickThemeSwitcher: React.FC<Props> = ({ requestClose }) => {
value={selectedTheme} value={selectedTheme}
label={_t("Space selection")} 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> </Dropdown>
</div> </div>
); );

View file

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

View file

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