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 snapshotpull/28788/head^2
parent
cefd94859c
commit
a47b3eb0ee
|
@ -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")) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 },
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
|
|
Loading…
Reference in New Issue