Conform more of the codebase to strictNullChecks (#10358

* Conform more of the codebase to `strictNullChecks`

* Fix types

* Iterate

* Iterate
This commit is contained in:
Michael Telatynski 2023-03-13 15:07:20 +00:00 committed by GitHub
parent 41d88ad6ae
commit 503df62191
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
76 changed files with 323 additions and 327 deletions

View file

@ -80,7 +80,7 @@ export default class DownloadActionButton extends React.PureComponent<IProps, IS
}
public render(): React.ReactNode {
let spinner: JSX.Element;
let spinner: JSX.Element | undefined;
if (this.state.loading) {
spinner = <Spinner w={18} h={18} />;
}

View file

@ -47,7 +47,7 @@ interface IProps {
interface IState {
canRedact: boolean;
sendStatus: EventStatus;
sendStatus: EventStatus | null;
}
export default class EditHistoryMessage extends React.PureComponent<IProps, IState> {
@ -59,12 +59,10 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
super(props);
const cli = MatrixClientPeg.get();
const { userId } = cli.credentials;
const userId = cli.getSafeUserId();
const event = this.props.mxEvent;
const room = cli.getRoom(event.getRoomId());
if (event.localRedactionEvent()) {
event.localRedactionEvent().on(MatrixEventEvent.Status, this.onAssociatedStatusChanged);
}
event.localRedactionEvent()?.on(MatrixEventEvent.Status, this.onAssociatedStatusChanged);
const canRedact = room.currentState.maySendRedactionForEvent(event, userId);
this.state = { canRedact, sendStatus: event.getAssociatedStatus() };
}
@ -121,9 +119,7 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
unmountPills(this.pills);
unmountTooltips(this.tooltips);
const event = this.props.mxEvent;
if (event.localRedactionEvent()) {
event.localRedactionEvent().off(MatrixEventEvent.Status, this.onAssociatedStatusChanged);
}
event.localRedactionEvent()?.off(MatrixEventEvent.Status, this.onAssociatedStatusChanged);
}
public componentDidUpdate(): void {
@ -133,12 +129,12 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
private renderActionBar(): JSX.Element {
// hide the button when already redacted
let redactButton: JSX.Element;
let redactButton: JSX.Element | undefined;
if (!this.props.mxEvent.isRedacted() && !this.props.isBaseEvent && this.state.canRedact) {
redactButton = <AccessibleButton onClick={this.onRedactClick}>{_t("Remove")}</AccessibleButton>;
}
let viewSourceButton: JSX.Element;
let viewSourceButton: JSX.Element | undefined;
if (SettingsStore.getValue("developerMode")) {
viewSourceButton = (
<AccessibleButton onClick={this.onViewSourceClick}>{_t("View Source")}</AccessibleButton>
@ -189,9 +185,8 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
}
const timestamp = formatTime(new Date(mxEvent.getTs()), this.props.isTwelveHour);
const isSending = ["sending", "queued", "encrypting"].indexOf(this.state.sendStatus) !== -1;
const classes = classNames({
mx_EventTile: true,
const isSending = ["sending", "queued", "encrypting"].includes(this.state.sendStatus!);
const classes = classNames("mx_EventTile", {
// Note: we keep the `sending` state class for tests, not for our styles
mx_EventTile_sending: isSending,
});

View file

@ -22,7 +22,7 @@ import { RovingAccessibleButton, useRovingTabIndex } from "../../../accessibilit
interface IProps {
ts: number;
onDatePicked?: (dateString: string) => void;
onDatePicked: (dateString: string) => void;
}
const JumpToDatePicker: React.FC<IProps> = ({ ts, onDatePicked }: IProps) => {

View file

@ -165,7 +165,7 @@ export default class LegacyCallEvent extends React.PureComponent<IProps, IState>
{this.props.timestamp}
</div>
);
} else if ([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason) {
} else if (!hangupReason || [CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) {
// workaround for https://github.com/vector-im/element-web/issues/5178
// it seems Android randomly sets a reason of "user hangup" which is
// interpreted as an error code :(

View file

@ -140,7 +140,7 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelati
error?.message === LocationShareError.MapStyleUrlNotConfigured ||
error?.message === LocationShareError.MapStyleUrlNotReachable;
const displayStatus = getBeaconDisplayStatus(
isLive,
!!isLive,
latestLocationState,
// if we are unable to display maps because it is not configured for the server
// don't display an error
@ -174,7 +174,7 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelati
map = (
<Map
id={mapId}
centerGeoUri={latestLocationState.uri}
centerGeoUri={latestLocationState?.uri}
onError={setError}
onClick={onClick}
className="mx_MBeaconBody_map"
@ -184,7 +184,7 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelati
map={map}
id={`${mapId}-marker`}
geoUri={latestLocationState.uri}
roomMember={markerRoomMember}
roomMember={markerRoomMember ?? undefined}
useMemberColor
/>
)}

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ComponentProps, createRef } from "react";
import React, { ComponentProps, createRef, ReactNode } from "react";
import { Blurhash } from "react-blurhash";
import classNames from "classnames";
import { CSSTransition, SwitchTransition } from "react-transition-group";
@ -47,8 +47,8 @@ enum Placeholder {
}
interface IState {
contentUrl?: string;
thumbUrl?: string;
contentUrl: string | null;
thumbUrl: string | null;
isAnimated?: boolean;
error?: Error;
imgError: boolean;
@ -78,6 +78,8 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
this.reconnectedListener = createReconnectedListener(this.clearError);
this.state = {
contentUrl: null,
thumbUrl: null,
imgError: false,
imgLoaded: false,
hover: false,
@ -126,7 +128,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
};
}
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", undefined, true);
}
};
@ -177,7 +179,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
this.setState({ imgLoaded: true, loadedImageDimensions });
};
private getContentUrl(): string {
private getContentUrl(): string | null {
// During export, the content url will point to the MSC, which will later point to a local url
if (this.props.forExport) return this.media.srcMxc;
return this.media.srcHttp;
@ -187,7 +189,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
return mediaFromContent(this.props.mxEvent.getContent());
}
private getThumbUrl(): string {
private getThumbUrl(): string | null {
// FIXME: we let images grow as wide as you like, rather than capped to 800x600.
// So either we need to support custom timeline widths here, or reimpose the cap, otherwise the
// thumbnail resolution will be unnecessarily reduced.
@ -242,8 +244,8 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
private async downloadImage(): Promise<void> {
if (this.state.contentUrl) return; // already downloaded
let thumbUrl: string;
let contentUrl: string;
let thumbUrl: string | null;
let contentUrl: string | null;
if (this.props.mediaEventHelper.media.isEncrypted) {
try {
[contentUrl, thumbUrl] = await Promise.all([
@ -276,7 +278,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
// If there is no included non-animated thumbnail then we will generate our own, we can't depend on the server
// because 1. encryption and 2. we can't ask the server specifically for a non-animated thumbnail.
if (isAnimated && !SettingsStore.getValue("autoplayGifs")) {
if (!thumbUrl || !content?.info.thumbnail_info || mayBeAnimated(content.info.thumbnail_info.mimetype)) {
if (!thumbUrl || !content?.info?.thumbnail_info || mayBeAnimated(content.info.thumbnail_info.mimetype)) {
const img = document.createElement("img");
const loadPromise = new Promise((resolve, reject) => {
img.onload = resolve;
@ -364,7 +366,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
}
}
protected getBanner(content: IMediaEventContent): JSX.Element {
protected getBanner(content: IMediaEventContent): ReactNode {
// Hide it for the threads list & the file panel where we show it as text anyway.
if (
[TimelineRenderingType.ThreadsList, TimelineRenderingType.File].includes(this.context.timelineRenderingType)
@ -429,9 +431,9 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
forcedHeight ?? this.props.maxImageHeight,
);
let img: JSX.Element;
let placeholder: JSX.Element;
let gifLabel: JSX.Element;
let img: JSX.Element | undefined;
let placeholder: JSX.Element | undefined;
let gifLabel: JSX.Element | undefined;
if (!this.props.forExport && !this.state.imgLoaded) {
const classes = classNames("mx_MImageBody_placeholder", {
@ -471,7 +473,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
gifLabel = <p className="mx_MImageBody_gifLabel">GIF</p>;
}
let banner: JSX.Element;
let banner: ReactNode | undefined;
if (this.state.showImage && this.state.hover) {
banner = this.getBanner(content);
}
@ -526,7 +528,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
}
// Overridden by MStickerBody
protected getPlaceholder(width: number, height: number): JSX.Element {
protected getPlaceholder(width: number, height: number): ReactNode {
const blurhash = this.props.mxEvent.getContent().info?.[BLURHASH_FIELD];
if (blurhash) {
@ -540,12 +542,12 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
}
// Overridden by MStickerBody
protected getTooltip(): JSX.Element {
protected getTooltip(): ReactNode {
return null;
}
// Overridden by MStickerBody
protected getFileBody(): string | JSX.Element {
protected getFileBody(): ReactNode {
if (this.props.forExport) return null;
/*
* In the room timeline or the thread context we don't need the download
@ -577,7 +579,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
}
let contentUrl = this.state.contentUrl;
let thumbUrl: string;
let thumbUrl: string | undefined;
if (this.props.forExport) {
contentUrl = this.props.mxEvent.getContent().url ?? this.props.mxEvent.getContent().file?.url;
thumbUrl = contentUrl;

View file

@ -41,7 +41,7 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
const widgetId = this.props.mxEvent.getStateKey();
const widget = WidgetStore.instance.getRoom(room.roomId, true).widgets.find((w) => w.id === widgetId);
let joinCopy = _t("Join the conference at the top of this room");
let joinCopy: string | null = _t("Join the conference at the top of this room");
if (widget && WidgetLayoutStore.instance.isInContainer(room, widget, Container.Right)) {
joinCopy = _t("Join the conference from the room information card on the right");
} else if (!widget) {

View file

@ -124,7 +124,7 @@ export default class MKeyVerificationRequest extends React.Component<IProps> {
let title: string;
let subtitle: string;
let stateNode: JSX.Element;
let stateNode: JSX.Element | undefined;
if (!request.canAccept) {
let stateLabel;

View file

@ -37,7 +37,7 @@ import { IBodyProps } from "./IBodyProps";
import { createReconnectedListener } from "../../../utils/connection";
interface IState {
error: Error;
error?: Error;
}
export default class MLocationBody extends React.Component<IBodyProps, IState> {
@ -58,9 +58,7 @@ export default class MLocationBody extends React.Component<IBodyProps, IState> {
this.reconnectedListener = createReconnectedListener(this.clearError);
this.state = {
error: undefined,
};
this.state = {};
}
private onClick = (): void => {
@ -149,7 +147,12 @@ export const LocationBodyContent: React.FC<LocationBodyContentProps> = ({
const mapElement = (
<Map id={mapId} centerGeoUri={geoUri} onClick={onClick} onError={onError} className="mx_MLocationBody_map">
{({ map }) => (
<SmartMarker map={map} id={`${mapId}-marker`} geoUri={geoUri} roomMember={markerRoomMember} />
<SmartMarker
map={map}
id={`${mapId}-marker`}
geoUri={geoUri}
roomMember={markerRoomMember ?? undefined}
/>
)}
</Map>
);

View file

@ -128,7 +128,7 @@ export function launchPollEditor(mxEvent: MatrixEvent, getRelationsForEvent?: Ge
PollCreateDialog,
{
room: MatrixClientPeg.get().getRoom(mxEvent.getRoomId()),
threadId: mxEvent.getThread()?.id ?? null,
threadId: mxEvent.getThread()?.id,
editingMxEvent: mxEvent,
},
"mx_CompoundDialog",

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, { ReactNode } from "react";
import MImageBody from "./MImageBody";
import { BLURHASH_FIELD } from "../../../utils/image-media";
@ -33,7 +33,7 @@ export default class MStickerBody extends MImageBody {
// MStickerBody doesn't need a wrapping `<a href=...>`, but it does need extra padding
// which is added by mx_MStickerBody_wrapper
protected wrapImage(contentUrl: string, children: React.ReactNode): JSX.Element {
let onClick = null;
let onClick: React.MouseEventHandler | undefined;
if (!this.state.showImage) {
onClick = this.onClick;
}
@ -46,7 +46,7 @@ export default class MStickerBody extends MImageBody {
}
// Placeholder to show in place of the sticker image if img onLoad hasn't fired yet.
protected getPlaceholder(width: number, height: number): JSX.Element {
protected getPlaceholder(width: number, height: number): ReactNode {
if (this.props.mxEvent.getContent().info?.[BLURHASH_FIELD]) return super.getPlaceholder(width, height);
return (
<img
@ -61,7 +61,7 @@ export default class MStickerBody extends MImageBody {
}
// Tooltip to show on mouse over
protected getTooltip(): JSX.Element {
protected getTooltip(): ReactNode {
const content = this.props.mxEvent && this.props.mxEvent.getContent();
if (!content || !content.body || !content.info || !content.info.w) return null;
@ -74,11 +74,11 @@ export default class MStickerBody extends MImageBody {
}
// Don't show "Download this_file.png ..."
protected getFileBody(): JSX.Element {
protected getFileBody(): ReactNode {
return null;
}
protected getBanner(content: IMediaEventContent): JSX.Element {
protected getBanner(content: IMediaEventContent): ReactNode {
return null; // we don't need a banner, we have a tooltip
}
}

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, { ReactNode } from "react";
import { decode } from "blurhash";
import { logger } from "matrix-js-sdk/src/logger";
@ -31,13 +31,13 @@ import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContex
import MediaProcessingError from "./shared/MediaProcessingError";
interface IState {
decryptedUrl?: string;
decryptedThumbnailUrl?: string;
decryptedBlob?: Blob;
decryptedUrl: string | null;
decryptedThumbnailUrl: string | null;
decryptedBlob: Blob | null;
error?: any;
fetchingData: boolean;
posterLoading: boolean;
blurhashUrl: string;
blurhashUrl: string | null;
}
export default class MVideoBody extends React.PureComponent<IBodyProps, IState> {
@ -61,21 +61,21 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
};
}
private getContentUrl(): string | null {
private getContentUrl(): string | undefined {
const content = this.props.mxEvent.getContent<IMediaEventContent>();
// During export, the content url will point to the MSC, which will later point to a local url
if (this.props.forExport) return content.file?.url || content.url;
if (this.props.forExport) return content.file?.url ?? content.url;
const media = mediaFromContent(content);
if (media.isEncrypted) {
return this.state.decryptedUrl;
return this.state.decryptedUrl ?? undefined;
} else {
return media.srcHttp;
return media.srcHttp ?? undefined;
}
}
private hasContentUrl(): boolean {
const url = this.getContentUrl();
return url && !url.startsWith("data:");
return !!url && !url.startsWith("data:");
}
private getThumbUrl(): string | null {
@ -227,7 +227,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
);
}
private getFileBody = (): JSX.Element => {
private getFileBody = (): ReactNode => {
if (this.props.forExport) return null;
return this.showFileBody && <MFileBody {...this.props} showGenericPlaceholder={false} />;
};
@ -271,7 +271,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
const contentUrl = this.getContentUrl();
const thumbUrl = this.getThumbUrl();
let poster = null;
let poster: string | undefined;
let preload = "metadata";
if (content.info && thumbUrl) {
poster = thumbUrl;

View file

@ -63,7 +63,7 @@ interface IOptionsButtonProps {
mxEvent: MatrixEvent;
// TODO: Types
getTile: () => any | null;
getReplyChain: () => ReplyChain;
getReplyChain: () => ReplyChain | null;
permalinkCreator: RoomPermalinkCreator;
onFocusChange: (menuDisplayed: boolean) => void;
getRelationsForEvent?: GetRelationsForEvent;
@ -97,10 +97,10 @@ const OptionsButton: React.FC<IOptionsButtonProps> = ({
[openMenu, onFocus],
);
let contextMenu: ReactElement | null;
if (menuDisplayed) {
let contextMenu: ReactElement | undefined;
if (menuDisplayed && button.current) {
const tile = getTile && getTile();
const replyChain = getReplyChain && getReplyChain();
const replyChain = getReplyChain();
const buttonRect = button.current.getBoundingClientRect();
contextMenu = (
@ -109,7 +109,7 @@ const OptionsButton: React.FC<IOptionsButtonProps> = ({
mxEvent={mxEvent}
permalinkCreator={permalinkCreator}
eventTileOps={tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined}
collapseReplyChain={replyChain && replyChain.canCollapse() ? replyChain.collapse : undefined}
collapseReplyChain={replyChain?.canCollapse() ? replyChain.collapse : undefined}
onFinished={closeMenu}
getRelationsForEvent={getRelationsForEvent}
/>
@ -148,8 +148,8 @@ const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusC
onFocusChange(menuDisplayed);
}, [onFocusChange, menuDisplayed]);
let contextMenu;
if (menuDisplayed) {
let contextMenu: JSX.Element | undefined;
if (menuDisplayed && button.current) {
const buttonRect = button.current.getBoundingClientRect();
contextMenu = (
<ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
@ -211,7 +211,7 @@ const ReplyInThreadButton: React.FC<IReplyInThreadButton> = ({ mxEvent }) => {
if (mxEvent.getThread() && !mxEvent.isThreadRoot) {
defaultDispatcher.dispatch<ShowThreadPayload>({
action: Action.ShowThread,
rootEvent: mxEvent.getThread().rootEvent,
rootEvent: mxEvent.getThread()!.rootEvent,
initialEvent: mxEvent,
scroll_into_view: true,
highlighted: true,
@ -293,7 +293,7 @@ interface IMessageActionBarProps {
reactions?: Relations | null | undefined;
// TODO: Types
getTile: () => any | null;
getReplyChain: () => ReplyChain | undefined;
getReplyChain: () => ReplyChain | null;
permalinkCreator?: RoomPermalinkCreator;
onFocusChange?: (menuDisplayed: boolean) => void;
toggleThreadExpanded: () => void;
@ -421,7 +421,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
};
public render(): React.ReactNode {
const toolbarOpts = [];
const toolbarOpts: JSX.Element[] = [];
if (canEditContent(this.props.mxEvent)) {
toolbarOpts.push(
<RovingAccessibleTooltipButton
@ -452,8 +452,8 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
// We show a different toolbar for failed events, so detect that first.
const mxEvent = this.props.mxEvent;
const editStatus = mxEvent.replacingEvent() && mxEvent.replacingEvent().status;
const redactStatus = mxEvent.localRedactionEvent() && mxEvent.localRedactionEvent().status;
const editStatus = mxEvent.replacingEvent()?.status;
const redactStatus = mxEvent.localRedactionEvent()?.status;
const allowCancel = canCancel(mxEvent.status) || canCancel(editStatus) || canCancel(redactStatus);
const isFailed = [mxEvent.status, editStatus, redactStatus].includes(EventStatus.NOT_SENT);
if (allowCancel && isFailed) {

View file

@ -58,7 +58,7 @@ interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper
}
export interface IOperableEventTile {
getEventTileOps(): IEventTileOps;
getEventTileOps(): IEventTileOps | null;
}
const baseBodyTypes = new Map<string, typeof React.Component>([
@ -159,12 +159,12 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
if (this.props.mxEvent.isDecryptionFailure()) {
BodyType = DecryptionFailureBody;
} else if (type && this.evTypes.has(type)) {
BodyType = this.evTypes.get(type);
BodyType = this.evTypes.get(type)!;
} else if (msgtype && this.bodyTypes.has(msgtype)) {
BodyType = this.bodyTypes.get(msgtype);
BodyType = this.bodyTypes.get(msgtype)!;
} else if (content.url) {
// Fallback to MFileBody if there's a content URL
BodyType = this.bodyTypes.get(MsgType.File);
BodyType = this.bodyTypes.get(MsgType.File)!;
} else {
// Fallback to UnknownBody otherwise if not redacted
BodyType = UnknownBody;

View file

@ -142,7 +142,7 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
this.forceUpdate();
};
private getMyReactions(): MatrixEvent[] {
private getMyReactions(): MatrixEvent[] | null {
const reactions = this.props.reactions;
if (!reactions) {
return null;
@ -206,7 +206,7 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
// Show the first MAX_ITEMS if there are MAX_ITEMS + 1 or more items.
// The "+ 1" ensure that the "show all" reveals something that takes up
// more space than the button itself.
let showAllButton: JSX.Element;
let showAllButton: JSX.Element | undefined;
if (items.length > MAX_ITEMS_WHEN_LIMITED + 1 && !showAll) {
items = items.slice(0, MAX_ITEMS_WHEN_LIMITED);
showAllButton = (

View file

@ -106,9 +106,9 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
}
const room = this.context.getRoom(mxEvent.getRoomId());
let label: string;
let label: string | undefined;
if (room) {
const senders = [];
const senders: string[] = [];
for (const reactionEvent of reactionEvents) {
const member = room.getMember(reactionEvent.getSender());
senders.push(member?.name || reactionEvent.getSender());

View file

@ -43,5 +43,7 @@ export default function SenderProfile({ mxEvent, onClick, withTooltip }: IProps)
emphasizeDisplayName={true}
withTooltip={withTooltip}
/>
) : null;
) : (
<></>
);
}

View file

@ -342,7 +342,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
if (node.tagName === "SPAN" && typeof node.getAttribute("data-mx-spoiler") === "string") {
const spoilerContainer = document.createElement("span");
const reason = node.getAttribute("data-mx-spoiler");
const reason = node.getAttribute("data-mx-spoiler") ?? undefined;
node.removeAttribute("data-mx-spoiler"); // we don't want to recurse
const spoiler = <Spoiler reason={reason} contentHtml={node.outerHTML} />;
@ -367,7 +367,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
const node = nodes[i];
if (node.tagName === "A" && node.getAttribute("href")) {
if (this.isLinkPreviewable(node)) {
links.push(node.getAttribute("href"));
links.push(node.getAttribute("href")!);
}
} else if (node.tagName === "PRE" || node.tagName === "CODE" || node.tagName === "BLOCKQUOTE") {
continue;
@ -380,7 +380,8 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
private isLinkPreviewable(node: Element): boolean {
// don't try to preview relative links
if (!node.getAttribute("href").startsWith("http://") && !node.getAttribute("href").startsWith("https://")) {
const href = node.getAttribute("href") ?? "";
if (!href.startsWith("http://") && !href.startsWith("https://")) {
return false;
}
@ -389,24 +390,24 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
// or from a full foo.bar/baz style schemeless URL) - or be a markdown-style
// link, in which case we check the target text differs from the link value.
// TODO: make this configurable?
if (node.textContent.indexOf("/") > -1) {
if (node.textContent?.includes("/")) {
return true;
}
const url = node.getAttribute("href");
const host = url.match(/^https?:\/\/(.*?)(\/|$)/)[1];
// never preview permalinks (if anything we should give a smart
// preview of the room/user they point to: nobody needs to be reminded
// what the matrix.to site looks like).
if (isPermalinkHost(host)) return false;
if (node.textContent?.toLowerCase().trim().startsWith(host.toLowerCase())) {
// it's a "foo.pl" style link
return false;
} else {
const url = node.getAttribute("href");
const host = url.match(/^https?:\/\/(.*?)(\/|$)/)[1];
// never preview permalinks (if anything we should give a smart
// preview of the room/user they point to: nobody needs to be reminded
// what the matrix.to site looks like).
if (isPermalinkHost(host)) return false;
if (node.textContent.toLowerCase().trim().startsWith(host.toLowerCase())) {
// it's a "foo.pl" style link
return false;
} else {
// it's a [foo bar](http://foo.com) style link
return true;
}
// it's a [foo bar](http://foo.com) style link
return true;
}
}
@ -434,7 +435,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
* to start with (e.g. pills, links in the content).
*/
private onBodyLinkClick = (e: MouseEvent): void => {
let target = e.target as HTMLLinkElement;
let target: HTMLLinkElement | null = e.target as HTMLLinkElement;
// links processed by linkifyjs have their own handler so don't handle those here
if (target.classList.contains(linkifyOpts.className as string)) return;
if (target.nodeName !== "A") {

View file

@ -34,16 +34,14 @@ interface IProps {
}
interface IState {
error: Error;
error?: Error;
}
export default class TileErrorBoundary extends React.Component<IProps, IState> {
public constructor(props: IProps) {
super(props);
this.state = {
error: null,
};
this.state = {};
}
public static getDerivedStateFromError(error: Error): Partial<IState> {