Skip to content

Commit

Permalink
Add option to display tooltip on link hover
Browse files Browse the repository at this point in the history
This makes it possible for platforms like Electron apps, which lack
a built-in URL preview in the status bar, to enable tooltip previews
of links.

Relates to: element-hq/element-web#6532
Signed-off-by: Johannes Marbach <johannesm@element.io>
  • Loading branch information
Johennes committed Apr 22, 2022
1 parent bbe0c94 commit f18cce8
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 0 deletions.
8 changes: 8 additions & 0 deletions src/BasePlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,14 @@ export default abstract class BasePlatform {
}
}

/**
* Returns true if the platform requires URL previews in tooltips, otherwise false.
* @returns {boolean} whether the platform requires URL previews in tooltips
*/
needsUrlTooltips(): boolean {
return false;
}

/**
* Returns a promise that resolves to a string representing the current version of the application.
*/
Expand Down
54 changes: 54 additions & 0 deletions src/HtmlUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ limitations under the License.
*/

import React, { ReactNode } from 'react';
import ReactDOM from 'react-dom';
import sanitizeHtml from 'sanitize-html';
import cheerio from 'cheerio';
import classNames from 'classnames';
Expand All @@ -35,6 +36,8 @@ import { getEmojiFromUnicode } from "./emoji";
import { mediaFromMxc } from "./customisations/Media";
import { ELEMENT_URL_PATTERN, options as linkifyMatrixOptions } from './linkify-matrix';
import { stripHTMLReply, stripPlainReply } from './utils/Reply';
import TextWithTooltip from './components/views/elements/TextWithTooltip';
import PlatformPeg from './PlatformPeg';

// Anything outside the basic multilingual plane will be a surrogate pair
const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/;
Expand Down Expand Up @@ -635,6 +638,57 @@ export function linkifyAndSanitizeHtml(dirtyHtml: string, options = linkifyMatri
return sanitizeHtml(linkifyString(dirtyHtml, options), sanitizeHtmlParams);
}

const getAbsoluteUrl = (() => {
let a: HTMLAnchorElement;

return (url: string) => {
if (!a) {
a = document.createElement('a');
}
a.href = url;
return a.href;
};
})();

/**
* Recurses depth-first through a DOM tree, adding tooltip previews for link elements.
*
* @param {Element[]} rootNodes - a list of sibling DOM nodes to traverse to try
* to add tooltips.
* @param {Element[]} ignoredNodes: a list of nodes to not recurse into.
*/
export function tooltipifyLinks(rootNodes: ArrayLike<Element>, ignoredNodes: Element[]) {
if (!PlatformPeg.get().needsUrlTooltips()) {
return;
}

let node = rootNodes[0];

while (node) {
let tooltipified = false;

if (ignoredNodes.indexOf(node) >= 0) {
node = node.nextSibling as Element;
continue;
}

if (node.tagName === "A" && node.getAttribute("href") && node.getAttribute("href") != node.textContent.trim()) {
const href = node.getAttribute("href");
const tooltip = <TextWithTooltip tooltip={getAbsoluteUrl(href)}>
<span dangerouslySetInnerHTML={{ __html: node.innerHTML }} />
</TextWithTooltip>;
ReactDOM.render(tooltip, node);
tooltipified = true;
}

if (node.childNodes && node.childNodes.length && !tooltipified) {
tooltipifyLinks(node.childNodes as NodeListOf<Element>, ignoredNodes);
}

node = node.nextSibling as Element;
}
}

/**
* Returns if a node is a block element or not.
* Only takes html nodes into account that are allowed in matrix messages.
Expand Down
9 changes: 9 additions & 0 deletions src/components/views/messages/EditHistoryMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,16 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
}
}

private tooltipifyLinks(): void {
// not present for redacted events
if (this.content.current) {
HtmlUtils.tooltipifyLinks(this.content.current.children, this.pills);
}
}

public componentDidMount(): void {
this.pillifyLinks();
this.tooltipifyLinks();
}

public componentWillUnmount(): void {
Expand All @@ -107,6 +115,7 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta

public componentDidUpdate(): void {
this.pillifyLinks();
this.tooltipifyLinks();
}

private renderActionBar(): JSX.Element {
Expand Down
1 change: 1 addition & 0 deletions src/components/views/messages/TextualBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
pillifyLinks([this.contentRef.current], this.props.mxEvent, this.pills);
HtmlUtils.linkifyElement(this.contentRef.current);
HtmlUtils.tooltipifyLinks([this.contentRef.current], this.pills);
this.calculateUrlPreview();

if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") {
Expand Down

0 comments on commit f18cce8

Please sign in to comment.