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,6 +17,7 @@ limitations under the License.
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
import type { Preset } from "matrix-js-sdk/src/@types/partials";
|
||||||
import type { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
|
import type { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
|
||||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
|
@ -32,7 +33,7 @@ function openSpaceContextMenu(spaceName: string): Chainable<JQuery> {
|
||||||
return cy.get(".mx_SpacePanel_contextMenu");
|
return cy.get(".mx_SpacePanel_contextMenu");
|
||||||
}
|
}
|
||||||
|
|
||||||
function spaceCreateOptions(spaceName: string): ICreateRoomOpts {
|
function spaceCreateOptions(spaceName: string, roomIds: string[] = []): ICreateRoomOpts {
|
||||||
return {
|
return {
|
||||||
creation_content: {
|
creation_content: {
|
||||||
type: "m.space",
|
type: "m.space",
|
||||||
|
@ -44,6 +45,7 @@ function spaceCreateOptions(spaceName: string): ICreateRoomOpts {
|
||||||
name: spaceName,
|
name: spaceName,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...roomIds.map(spaceChildInitialState),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -283,4 +285,29 @@ describe("Spaces", () => {
|
||||||
cy.checkA11y(undefined, axeOptions);
|
cy.checkA11y(undefined, axeOptions);
|
||||||
cy.get(".mx_SpacePanel").percySnapshotElement("Space panel expanded", { widths: [258] });
|
cy.get(".mx_SpacePanel").percySnapshotElement("Space panel expanded", { widths: [258] });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not soft crash when joining a room from space hierarchy which has a link in its topic", () => {
|
||||||
|
cy.getBot(homeserver, { displayName: "BotBob" }).then({ timeout: 10000 }, async (bot) => {
|
||||||
|
const { room_id: roomId } = await bot.createRoom({
|
||||||
|
preset: "public_chat" as Preset,
|
||||||
|
name: "Test Room",
|
||||||
|
topic: "This is a topic https://github.com/matrix-org/matrix-react-sdk/pull/10060 with a link",
|
||||||
|
});
|
||||||
|
const { room_id: spaceId } = await bot.createRoom(spaceCreateOptions("Test Space", [roomId]));
|
||||||
|
await bot.invite(spaceId, user.userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.getSpacePanelButton("Test Space").should("exist");
|
||||||
|
cy.wait(500); // without this we can end up clicking too quickly and it ends up having no effect
|
||||||
|
cy.viewSpaceByName("Test Space");
|
||||||
|
cy.contains(".mx_AccessibleButton", "Accept").click();
|
||||||
|
|
||||||
|
cy.contains(".mx_SpaceHierarchy_roomTile.mx_AccessibleButton", "Test Room").within(() => {
|
||||||
|
cy.contains("Join").should("exist").realHover().click();
|
||||||
|
cy.contains("View", { timeout: 5000 }).should("exist").click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert we get shown the new room intro, and thus not the soft crash screen
|
||||||
|
cy.get(".mx_NewRoomIntro").should("exist");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -86,6 +86,7 @@
|
||||||
"jszip": "^3.7.0",
|
"jszip": "^3.7.0",
|
||||||
"katex": "^0.16.0",
|
"katex": "^0.16.0",
|
||||||
"linkify-element": "4.0.0-beta.4",
|
"linkify-element": "4.0.0-beta.4",
|
||||||
|
"linkify-react": "4.0.0-beta.4",
|
||||||
"linkify-string": "4.0.0-beta.4",
|
"linkify-string": "4.0.0-beta.4",
|
||||||
"linkifyjs": "4.0.0-beta.4",
|
"linkifyjs": "4.0.0-beta.4",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
|
|
|
@ -17,16 +17,17 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ReactNode } from "react";
|
import React, { 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";
|
||||||
import EMOJIBASE_REGEX from "emojibase-regex";
|
import EMOJIBASE_REGEX from "emojibase-regex";
|
||||||
import { split } from "lodash";
|
import { merge, split } from "lodash";
|
||||||
import katex from "katex";
|
import katex from "katex";
|
||||||
import { decode } from "html-entities";
|
import { decode } from "html-entities";
|
||||||
import { IContent } from "matrix-js-sdk/src/models/event";
|
import { IContent } from "matrix-js-sdk/src/models/event";
|
||||||
import { Optional } from "matrix-events-sdk";
|
import { Optional } from "matrix-events-sdk";
|
||||||
|
import _Linkify from "linkify-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
_linkifyElement,
|
_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'.
|
* 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 { _t, _td, ITranslatableError, newTranslatableError } from "./languageHandler";
|
||||||
import Modal from "./Modal";
|
import Modal from "./Modal";
|
||||||
import MultiInviter from "./utils/MultiInviter";
|
import MultiInviter from "./utils/MultiInviter";
|
||||||
import { linkifyElement, topicToHtml } from "./HtmlUtils";
|
import { Linkify, topicToHtml } from "./HtmlUtils";
|
||||||
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
||||||
import WidgetUtils from "./utils/WidgetUtils";
|
import WidgetUtils from "./utils/WidgetUtils";
|
||||||
import { textToHtmlRainbow } from "./utils/colour";
|
import { textToHtmlRainbow } from "./utils/colour";
|
||||||
|
@ -501,14 +501,11 @@ export const Commands = [
|
||||||
? ContentHelpers.parseTopicContent(content)
|
? ContentHelpers.parseTopicContent(content)
|
||||||
: { text: _t("This room has no topic.") };
|
: { text: _t("This room has no topic.") };
|
||||||
|
|
||||||
const ref = (e): void => {
|
const body = topicToHtml(topic.text, topic.html, undefined, true);
|
||||||
if (e) linkifyElement(e);
|
|
||||||
};
|
|
||||||
const body = topicToHtml(topic.text, topic.html, ref, true);
|
|
||||||
|
|
||||||
Modal.createDialog(InfoDialog, {
|
Modal.createDialog(InfoDialog, {
|
||||||
title: room.name,
|
title: room.name,
|
||||||
description: <div ref={ref}>{body}</div>,
|
description: <Linkify>{body}</Linkify>,
|
||||||
hasCloseButton: true,
|
hasCloseButton: true,
|
||||||
className: "markdown-body",
|
className: "markdown-body",
|
||||||
});
|
});
|
||||||
|
|
|
@ -136,9 +136,9 @@ import { SdkContextClass, SDKContext } from "../../contexts/SDKContext";
|
||||||
import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings";
|
import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings";
|
||||||
import { cleanUpBroadcasts, VoiceBroadcastResumer } from "../../voice-broadcast";
|
import { cleanUpBroadcasts, VoiceBroadcastResumer } from "../../voice-broadcast";
|
||||||
import GenericToast from "../views/toasts/GenericToast";
|
import GenericToast from "../views/toasts/GenericToast";
|
||||||
import { Linkify } from "../views/elements/Linkify";
|
|
||||||
import RovingSpotlightDialog, { Filter } from "../views/dialogs/spotlight/SpotlightDialog";
|
import RovingSpotlightDialog, { Filter } from "../views/dialogs/spotlight/SpotlightDialog";
|
||||||
import { findDMForUser } from "../../utils/dm/findDMForUser";
|
import { findDMForUser } from "../../utils/dm/findDMForUser";
|
||||||
|
import { Linkify } from "../../HtmlUtils";
|
||||||
|
|
||||||
// legacy export
|
// legacy export
|
||||||
export { default as Views } from "../../Views";
|
export { default as Views } from "../../Views";
|
||||||
|
|
|
@ -51,7 +51,7 @@ import TextWithTooltip from "../views/elements/TextWithTooltip";
|
||||||
import { useStateToggle } from "../../hooks/useStateToggle";
|
import { useStateToggle } from "../../hooks/useStateToggle";
|
||||||
import { getChildOrder } from "../../stores/spaces/SpaceStore";
|
import { getChildOrder } from "../../stores/spaces/SpaceStore";
|
||||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||||
import { linkifyElement, topicToHtml } from "../../HtmlUtils";
|
import { Linkify, topicToHtml } from "../../HtmlUtils";
|
||||||
import { useDispatcher } from "../../hooks/useDispatcher";
|
import { useDispatcher } from "../../hooks/useDispatcher";
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import { IState, RovingTabIndexProvider, useRovingTabIndex } from "../../accessibility/RovingTabIndex";
|
import { IState, RovingTabIndexProvider, useRovingTabIndex } from "../../accessibility/RovingTabIndex";
|
||||||
|
@ -210,6 +210,25 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
topic = room.topic;
|
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;
|
let joinedSection: ReactElement | undefined;
|
||||||
if (joinedRoom) {
|
if (joinedRoom) {
|
||||||
joinedSection = <div className="mx_SpaceHierarchy_roomTile_joined">{_t("Joined")}</div>;
|
joinedSection = <div className="mx_SpaceHierarchy_roomTile_joined">{_t("Joined")}</div>;
|
||||||
|
@ -231,19 +250,9 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
{joinedSection}
|
{joinedSection}
|
||||||
{suggestedSection}
|
{suggestedSection}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="mx_SpaceHierarchy_roomTile_info">
|
||||||
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();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{description}
|
{description}
|
||||||
{topic && " · "}
|
{topicSection}
|
||||||
{topic}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_SpaceHierarchy_actions">
|
<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 { useDispatcher } from "../../../hooks/useDispatcher";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import AccessibleButton from "./AccessibleButton";
|
import AccessibleButton from "./AccessibleButton";
|
||||||
import { Linkify } from "./Linkify";
|
|
||||||
import TooltipTarget from "./TooltipTarget";
|
import TooltipTarget from "./TooltipTarget";
|
||||||
import { topicToHtml } from "../../../HtmlUtils";
|
import { Linkify, topicToHtml } from "../../../HtmlUtils";
|
||||||
|
|
||||||
interface IProps extends React.HTMLProps<HTMLDivElement> {
|
interface IProps extends React.HTMLProps<HTMLDivElement> {
|
||||||
room?: Room;
|
room?: Room;
|
||||||
|
@ -71,12 +70,14 @@ export default function RoomTopic({ room, ...props }: IProps): JSX.Element {
|
||||||
description: (
|
description: (
|
||||||
<div>
|
<div>
|
||||||
<Linkify
|
<Linkify
|
||||||
as="p"
|
options={{
|
||||||
onClick={(ev: MouseEvent) => {
|
attributes: {
|
||||||
if ((ev.target as HTMLElement).tagName.toUpperCase() === "A") {
|
onClick() {
|
||||||
modal.close();
|
modal.close();
|
||||||
}
|
},
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
|
as="p"
|
||||||
>
|
>
|
||||||
{body}
|
{body}
|
||||||
</Linkify>
|
</Linkify>
|
||||||
|
|
|
@ -436,7 +436,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
||||||
private onBodyLinkClick = (e: MouseEvent): void => {
|
private onBodyLinkClick = (e: MouseEvent): void => {
|
||||||
let target = e.target as HTMLLinkElement;
|
let target = e.target as HTMLLinkElement;
|
||||||
// links processed by linkifyjs have their own handler so don't handle those here
|
// 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") {
|
if (target.nodeName !== "A") {
|
||||||
// Jump to parent as the `<a>` may contain children, e.g. an anchor wrapping an inline code section
|
// Jump to parent as the `<a>` may contain children, e.g. an anchor wrapping an inline code section
|
||||||
target = target.closest<HTMLLinkElement>("a");
|
target = target.closest<HTMLLinkElement>("a");
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { decode } from "html-entities";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { IPreviewUrlResponse } from "matrix-js-sdk/src/client";
|
import { IPreviewUrlResponse } from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
import { linkifyElement } from "../../../HtmlUtils";
|
import { Linkify } from "../../../HtmlUtils";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import * as ImageUtils from "../../../ImageUtils";
|
import * as ImageUtils from "../../../ImageUtils";
|
||||||
|
@ -35,21 +35,8 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class LinkPreviewWidget extends React.Component<IProps> {
|
export default class LinkPreviewWidget extends React.Component<IProps> {
|
||||||
private readonly description = createRef<HTMLDivElement>();
|
|
||||||
private image = createRef<HTMLImageElement>();
|
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 => {
|
private onImageClick = (ev): void => {
|
||||||
const p = this.props.preview;
|
const p = this.props.preview;
|
||||||
if (ev.button != 0 || ev.metaKey) return;
|
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>
|
<span className="mx_LinkPreviewWidget_siteName">{" - " + p["og:site_name"]}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_LinkPreviewWidget_description" ref={this.description}>
|
<div className="mx_LinkPreviewWidget_description">
|
||||||
{description}
|
<Linkify>{description}</Linkify>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -37,7 +37,7 @@ const RoomInfoLine: FC<IProps> = ({ room }) => {
|
||||||
const summary = useAsyncMemo(async (): Promise<Awaited<ReturnType<MatrixClient["getRoomSummary"]>> | null> => {
|
const summary = useAsyncMemo(async (): Promise<Awaited<ReturnType<MatrixClient["getRoomSummary"]>> | null> => {
|
||||||
if (room.getMyMembership() !== "invite") return null;
|
if (room.getMyMembership() !== "invite") return null;
|
||||||
try {
|
try {
|
||||||
return room.client.getRoomSummary(room.roomId);
|
return await room.client.getRoomSummary(room.roomId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as linkifyjs from "linkifyjs";
|
import * as linkifyjs from "linkifyjs";
|
||||||
import { registerCustomProtocol, registerPlugin } from "linkifyjs";
|
import { Opts, registerCustomProtocol, registerPlugin } from "linkifyjs";
|
||||||
import linkifyElement from "linkify-element";
|
import linkifyElement from "linkify-element";
|
||||||
import linkifyString from "linkify-string";
|
import linkifyString from "linkify-string";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
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/" +
|
"(?:app|beta|staging|develop)\\.element\\.io/" +
|
||||||
")(#.*)";
|
")(#.*)";
|
||||||
|
|
||||||
export const options = {
|
export const options: Opts = {
|
||||||
events: function (href: string, type: Type | string): Partial<GlobalEventHandlers> {
|
events: function (href: string, type: string): Partial<GlobalEventHandlers> {
|
||||||
switch (type) {
|
switch (type as Type) {
|
||||||
case Type.URL: {
|
case Type.URL: {
|
||||||
// intercept local permalinks to users and show them like userids (in userinfo of current room)
|
// intercept local permalinks to users and show them like userids (in userinfo of current room)
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -62,14 +62,12 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
|
||||||
<div
|
<div
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<span
|
<span
|
||||||
dir="auto"
|
dir="auto"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div
|
||||||
class="mx_RoomView_body"
|
class="mx_RoomView_body"
|
||||||
|
@ -161,14 +159,12 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
|
||||||
<div
|
<div
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<span
|
<span
|
||||||
dir="auto"
|
dir="auto"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
<main
|
<main
|
||||||
class="mx_RoomView_body"
|
class="mx_RoomView_body"
|
||||||
|
@ -356,14 +352,12 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
||||||
<div
|
<div
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<span
|
<span
|
||||||
dir="auto"
|
dir="auto"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
<main
|
<main
|
||||||
class="mx_RoomView_body"
|
class="mx_RoomView_body"
|
||||||
|
@ -623,14 +617,12 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
||||||
<div
|
<div
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<span
|
<span
|
||||||
dir="auto"
|
dir="auto"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
<main
|
<main
|
||||||
class="mx_RoomView_body"
|
class="mx_RoomView_body"
|
||||||
|
|
|
@ -1,82 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2021 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 { fireEvent, render } from "@testing-library/react";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
|
|
||||||
import { Linkify } from "../../../../src/components/views/elements/Linkify";
|
|
||||||
|
|
||||||
describe("Linkify", () => {
|
|
||||||
it("linkifies the context", () => {
|
|
||||||
const { container } = render(<Linkify>https://perdu.com</Linkify>);
|
|
||||||
expect(container.innerHTML).toBe(
|
|
||||||
'<div><a href="https://perdu.com" class="linkified" target="_blank" rel="noreferrer noopener">' +
|
|
||||||
"https://perdu.com" +
|
|
||||||
"</a></div>",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("correctly linkifies a room alias", () => {
|
|
||||||
const { container } = render(<Linkify>#element-web:matrix.org</Linkify>);
|
|
||||||
expect(container.innerHTML).toBe(
|
|
||||||
"<div>" +
|
|
||||||
'<a href="https://matrix.to/#/#element-web:matrix.org" class="linkified" rel="noreferrer noopener">' +
|
|
||||||
"#element-web:matrix.org" +
|
|
||||||
"</a></div>",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("changes the root tag name", () => {
|
|
||||||
const TAG_NAME = "p";
|
|
||||||
|
|
||||||
const { container } = render(<Linkify as={TAG_NAME}>Hello world!</Linkify>);
|
|
||||||
|
|
||||||
expect(container.querySelectorAll("p")).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("relinkifies on update", () => {
|
|
||||||
function DummyTest() {
|
|
||||||
const [n, setN] = useState(0);
|
|
||||||
function onClick() {
|
|
||||||
setN(n + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// upon clicking the element, change the content, and expect
|
|
||||||
// linkify to update
|
|
||||||
return (
|
|
||||||
<div onClick={onClick}>
|
|
||||||
<Linkify>{n % 2 === 0 ? "https://perdu.com" : "https://matrix.org"}</Linkify>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { container } = render(<DummyTest />);
|
|
||||||
|
|
||||||
expect(container.innerHTML).toBe(
|
|
||||||
"<div><div>" +
|
|
||||||
'<a href="https://perdu.com" class="linkified" target="_blank" rel="noreferrer noopener">' +
|
|
||||||
"https://perdu.com" +
|
|
||||||
"</a></div></div>",
|
|
||||||
);
|
|
||||||
|
|
||||||
fireEvent.click(container.querySelector("div"));
|
|
||||||
|
|
||||||
expect(container.innerHTML).toBe(
|
|
||||||
"<div><div>" +
|
|
||||||
'<a href="https://matrix.org" class="linkified" target="_blank" rel="noreferrer noopener">' +
|
|
||||||
"https://matrix.org" +
|
|
||||||
"</a></div></div>",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -6258,6 +6258,11 @@ linkify-element@4.0.0-beta.4:
|
||||||
resolved "https://registry.yarnpkg.com/linkify-element/-/linkify-element-4.0.0-beta.4.tgz#31bb5dff7430c4debc34030466bd8f3e297793a7"
|
resolved "https://registry.yarnpkg.com/linkify-element/-/linkify-element-4.0.0-beta.4.tgz#31bb5dff7430c4debc34030466bd8f3e297793a7"
|
||||||
integrity sha512-dsu5qxk6MhQHxXUlPjul33JknQPx7Iv/N8zisH4JtV31qVk0qZg/5gn10Hr76GlMuixcdcxVvGHNfVcvbut13w==
|
integrity sha512-dsu5qxk6MhQHxXUlPjul33JknQPx7Iv/N8zisH4JtV31qVk0qZg/5gn10Hr76GlMuixcdcxVvGHNfVcvbut13w==
|
||||||
|
|
||||||
|
linkify-react@4.0.0-beta.4:
|
||||||
|
version "4.0.0-beta.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/linkify-react/-/linkify-react-4.0.0-beta.4.tgz#75311ade523a52d43054dd841d724d746d43f60d"
|
||||||
|
integrity sha512-o4vFe28vtk6i8a6tbtkLyusIyhLJSYoHC3gEpmJEVqi6Hy3aguVEenYmtaOjmAQehDrBYeHv9s4qcneZOf7SWQ==
|
||||||
|
|
||||||
linkify-string@4.0.0-beta.4:
|
linkify-string@4.0.0-beta.4:
|
||||||
version "4.0.0-beta.4"
|
version "4.0.0-beta.4"
|
||||||
resolved "https://registry.yarnpkg.com/linkify-string/-/linkify-string-4.0.0-beta.4.tgz#0982509bc6ce81c554bff8d7121057193b84ea32"
|
resolved "https://registry.yarnpkg.com/linkify-string/-/linkify-string-4.0.0-beta.4.tgz#0982509bc6ce81c554bff8d7121057193b84ea32"
|
||||||
|
|
Loading…
Reference in a new issue