Switch to linkify-react for element Linkification as it handles React subtrees without exploding (#10060
* Switch to linkify-react instead of our faulty implementation Fixes a series of soft crashes where errors include "The node to be removed is not a child of this node." * Improve types * Fix types * Update snapshots * Add test * Fix test
This commit is contained in:
parent
089557005a
commit
2bde31dcff
15 changed files with 101 additions and 193 deletions
|
@ -17,16 +17,17 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ReactNode } from "react";
|
||||
import React, { ReactElement, ReactNode } from "react";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import cheerio from "cheerio";
|
||||
import classNames from "classnames";
|
||||
import EMOJIBASE_REGEX from "emojibase-regex";
|
||||
import { split } from "lodash";
|
||||
import { merge, split } from "lodash";
|
||||
import katex from "katex";
|
||||
import { decode } from "html-entities";
|
||||
import { IContent } from "matrix-js-sdk/src/models/event";
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
import _Linkify from "linkify-react";
|
||||
|
||||
import {
|
||||
_linkifyElement,
|
||||
|
@ -682,6 +683,15 @@ export function topicToHtml(
|
|||
);
|
||||
}
|
||||
|
||||
/* Wrapper around linkify-react merging in our default linkify options */
|
||||
export function Linkify({ as, options, children }: React.ComponentProps<typeof _Linkify>): ReactElement {
|
||||
return (
|
||||
<_Linkify as={as} options={merge({}, linkifyMatrixOptions, options)}>
|
||||
{children}
|
||||
</_Linkify>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Linkifies the given string. This is a wrapper around 'linkifyjs/string'.
|
||||
*
|
||||
|
|
|
@ -33,7 +33,7 @@ import dis from "./dispatcher/dispatcher";
|
|||
import { _t, _td, ITranslatableError, newTranslatableError } from "./languageHandler";
|
||||
import Modal from "./Modal";
|
||||
import MultiInviter from "./utils/MultiInviter";
|
||||
import { linkifyElement, topicToHtml } from "./HtmlUtils";
|
||||
import { Linkify, topicToHtml } from "./HtmlUtils";
|
||||
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
||||
import WidgetUtils from "./utils/WidgetUtils";
|
||||
import { textToHtmlRainbow } from "./utils/colour";
|
||||
|
@ -501,14 +501,11 @@ export const Commands = [
|
|||
? ContentHelpers.parseTopicContent(content)
|
||||
: { text: _t("This room has no topic.") };
|
||||
|
||||
const ref = (e): void => {
|
||||
if (e) linkifyElement(e);
|
||||
};
|
||||
const body = topicToHtml(topic.text, topic.html, ref, true);
|
||||
const body = topicToHtml(topic.text, topic.html, undefined, true);
|
||||
|
||||
Modal.createDialog(InfoDialog, {
|
||||
title: room.name,
|
||||
description: <div ref={ref}>{body}</div>,
|
||||
description: <Linkify>{body}</Linkify>,
|
||||
hasCloseButton: true,
|
||||
className: "markdown-body",
|
||||
});
|
||||
|
|
|
@ -136,9 +136,9 @@ import { SdkContextClass, SDKContext } from "../../contexts/SDKContext";
|
|||
import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings";
|
||||
import { cleanUpBroadcasts, VoiceBroadcastResumer } from "../../voice-broadcast";
|
||||
import GenericToast from "../views/toasts/GenericToast";
|
||||
import { Linkify } from "../views/elements/Linkify";
|
||||
import RovingSpotlightDialog, { Filter } from "../views/dialogs/spotlight/SpotlightDialog";
|
||||
import { findDMForUser } from "../../utils/dm/findDMForUser";
|
||||
import { Linkify } from "../../HtmlUtils";
|
||||
|
||||
// legacy export
|
||||
export { default as Views } from "../../Views";
|
||||
|
|
|
@ -51,7 +51,7 @@ import TextWithTooltip from "../views/elements/TextWithTooltip";
|
|||
import { useStateToggle } from "../../hooks/useStateToggle";
|
||||
import { getChildOrder } from "../../stores/spaces/SpaceStore";
|
||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||
import { linkifyElement, topicToHtml } from "../../HtmlUtils";
|
||||
import { Linkify, topicToHtml } from "../../HtmlUtils";
|
||||
import { useDispatcher } from "../../hooks/useDispatcher";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { IState, RovingTabIndexProvider, useRovingTabIndex } from "../../accessibility/RovingTabIndex";
|
||||
|
@ -210,6 +210,25 @@ const Tile: React.FC<ITileProps> = ({
|
|||
topic = room.topic;
|
||||
}
|
||||
|
||||
let topicSection: ReactNode | undefined;
|
||||
if (topic) {
|
||||
topicSection = (
|
||||
<Linkify
|
||||
options={{
|
||||
attributes: {
|
||||
onClick(ev: MouseEvent) {
|
||||
// prevent clicks on links from bubbling up to the room tile
|
||||
ev.stopPropagation();
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{" · "}
|
||||
{topic}
|
||||
</Linkify>
|
||||
);
|
||||
}
|
||||
|
||||
let joinedSection: ReactElement | undefined;
|
||||
if (joinedRoom) {
|
||||
joinedSection = <div className="mx_SpaceHierarchy_roomTile_joined">{_t("Joined")}</div>;
|
||||
|
@ -231,19 +250,9 @@ const Tile: React.FC<ITileProps> = ({
|
|||
{joinedSection}
|
||||
{suggestedSection}
|
||||
</div>
|
||||
<div
|
||||
className="mx_SpaceHierarchy_roomTile_info"
|
||||
ref={(e) => e && linkifyElement(e)}
|
||||
onClick={(ev) => {
|
||||
// prevent clicks on links from bubbling up to the room tile
|
||||
if ((ev.target as HTMLElement).tagName === "A") {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="mx_SpaceHierarchy_roomTile_info">
|
||||
{description}
|
||||
{topic && " · "}
|
||||
{topic}
|
||||
{topicSection}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_SpaceHierarchy_actions">
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useLayoutEffect, useRef } from "react";
|
||||
|
||||
import { linkifyElement } from "../../../HtmlUtils";
|
||||
|
||||
interface Props {
|
||||
as?: string;
|
||||
children: React.ReactNode;
|
||||
onClick?: (ev: MouseEvent) => void;
|
||||
}
|
||||
|
||||
export function Linkify({ as = "div", children, onClick }: Props): JSX.Element {
|
||||
const ref = useRef();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
linkifyElement(ref.current);
|
||||
}, [children]);
|
||||
|
||||
return React.createElement(as, {
|
||||
children,
|
||||
ref,
|
||||
onClick,
|
||||
});
|
||||
}
|
|
@ -29,9 +29,8 @@ import InfoDialog from "../dialogs/InfoDialog";
|
|||
import { useDispatcher } from "../../../hooks/useDispatcher";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import { Linkify } from "./Linkify";
|
||||
import TooltipTarget from "./TooltipTarget";
|
||||
import { topicToHtml } from "../../../HtmlUtils";
|
||||
import { Linkify, topicToHtml } from "../../../HtmlUtils";
|
||||
|
||||
interface IProps extends React.HTMLProps<HTMLDivElement> {
|
||||
room?: Room;
|
||||
|
@ -71,12 +70,14 @@ export default function RoomTopic({ room, ...props }: IProps): JSX.Element {
|
|||
description: (
|
||||
<div>
|
||||
<Linkify
|
||||
as="p"
|
||||
onClick={(ev: MouseEvent) => {
|
||||
if ((ev.target as HTMLElement).tagName.toUpperCase() === "A") {
|
||||
modal.close();
|
||||
}
|
||||
options={{
|
||||
attributes: {
|
||||
onClick() {
|
||||
modal.close();
|
||||
},
|
||||
},
|
||||
}}
|
||||
as="p"
|
||||
>
|
||||
{body}
|
||||
</Linkify>
|
||||
|
|
|
@ -436,7 +436,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
|||
private onBodyLinkClick = (e: MouseEvent): void => {
|
||||
let target = e.target as HTMLLinkElement;
|
||||
// links processed by linkifyjs have their own handler so don't handle those here
|
||||
if (target.classList.contains(linkifyOpts.className)) return;
|
||||
if (target.classList.contains(linkifyOpts.className as string)) return;
|
||||
if (target.nodeName !== "A") {
|
||||
// Jump to parent as the `<a>` may contain children, e.g. an anchor wrapping an inline code section
|
||||
target = target.closest<HTMLLinkElement>("a");
|
||||
|
|
|
@ -19,7 +19,7 @@ import { decode } from "html-entities";
|
|||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { IPreviewUrlResponse } from "matrix-js-sdk/src/client";
|
||||
|
||||
import { linkifyElement } from "../../../HtmlUtils";
|
||||
import { Linkify } from "../../../HtmlUtils";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import Modal from "../../../Modal";
|
||||
import * as ImageUtils from "../../../ImageUtils";
|
||||
|
@ -35,21 +35,8 @@ interface IProps {
|
|||
}
|
||||
|
||||
export default class LinkPreviewWidget extends React.Component<IProps> {
|
||||
private readonly description = createRef<HTMLDivElement>();
|
||||
private image = createRef<HTMLImageElement>();
|
||||
|
||||
public componentDidMount(): void {
|
||||
if (this.description.current) {
|
||||
linkifyElement(this.description.current);
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate(): void {
|
||||
if (this.description.current) {
|
||||
linkifyElement(this.description.current);
|
||||
}
|
||||
}
|
||||
|
||||
private onImageClick = (ev): void => {
|
||||
const p = this.props.preview;
|
||||
if (ev.button != 0 || ev.metaKey) return;
|
||||
|
@ -155,8 +142,8 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
|
|||
<span className="mx_LinkPreviewWidget_siteName">{" - " + p["og:site_name"]}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mx_LinkPreviewWidget_description" ref={this.description}>
|
||||
{description}
|
||||
<div className="mx_LinkPreviewWidget_description">
|
||||
<Linkify>{description}</Linkify>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -37,7 +37,7 @@ const RoomInfoLine: FC<IProps> = ({ room }) => {
|
|||
const summary = useAsyncMemo(async (): Promise<Awaited<ReturnType<MatrixClient["getRoomSummary"]>> | null> => {
|
||||
if (room.getMyMembership() !== "invite") return null;
|
||||
try {
|
||||
return room.client.getRoomSummary(room.roomId);
|
||||
return await room.client.getRoomSummary(room.roomId);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import * as linkifyjs from "linkifyjs";
|
||||
import { registerCustomProtocol, registerPlugin } from "linkifyjs";
|
||||
import { Opts, registerCustomProtocol, registerPlugin } from "linkifyjs";
|
||||
import linkifyElement from "linkify-element";
|
||||
import linkifyString from "linkify-string";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
|
@ -139,9 +139,9 @@ export const ELEMENT_URL_PATTERN =
|
|||
"(?:app|beta|staging|develop)\\.element\\.io/" +
|
||||
")(#.*)";
|
||||
|
||||
export const options = {
|
||||
events: function (href: string, type: Type | string): Partial<GlobalEventHandlers> {
|
||||
switch (type) {
|
||||
export const options: Opts = {
|
||||
events: function (href: string, type: string): Partial<GlobalEventHandlers> {
|
||||
switch (type as Type) {
|
||||
case Type.URL: {
|
||||
// intercept local permalinks to users and show them like userids (in userinfo of current room)
|
||||
try {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue