2019-06-26 17:13:15 +03:00
|
|
|
/*
|
2021-01-27 14:46:20 +03:00
|
|
|
Copyright 2019, 2020, 2021 The Matrix.org Foundation C.I.C.
|
2019-06-26 17:13:15 +03:00
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2020-01-06 16:28:29 +03:00
|
|
|
import React from "react";
|
2019-05-31 17:52:27 +03:00
|
|
|
import ReactDOM from 'react-dom';
|
2019-12-21 00:13:46 +03:00
|
|
|
import {MatrixClientPeg} from '../MatrixClientPeg';
|
2019-05-31 17:52:27 +03:00
|
|
|
import SettingsStore from "../settings/SettingsStore";
|
2019-12-20 05:08:43 +03:00
|
|
|
import {PushProcessor} from 'matrix-js-sdk/src/pushprocessor';
|
2021-01-27 14:46:20 +03:00
|
|
|
import Pill from "../components/views/elements/Pill";
|
|
|
|
import { parseAppLocalLink } from "./permalinks/Permalinks";
|
2019-05-31 17:52:27 +03:00
|
|
|
|
2020-02-23 02:51:30 +03:00
|
|
|
/**
|
|
|
|
* Recurses depth-first through a DOM tree, converting matrix.to links
|
|
|
|
* into pills based on the context of a given room. Returns a list of
|
|
|
|
* the resulting React nodes so they can be unmounted rather than leaking.
|
|
|
|
*
|
|
|
|
* @param {Node[]} nodes - a list of sibling DOM nodes to traverse to try
|
|
|
|
* to turn into pills.
|
|
|
|
* @param {MatrixEvent} mxEvent - the matrix event which the DOM nodes are
|
|
|
|
* part of representing.
|
|
|
|
* @param {Node[]} pills: an accumulator of the DOM nodes which contain
|
|
|
|
* React components which have been mounted as part of this.
|
|
|
|
* The initial caller should pass in an empty array to seed the accumulator.
|
|
|
|
*/
|
|
|
|
export function pillifyLinks(nodes, mxEvent, pills) {
|
2019-05-31 17:52:27 +03:00
|
|
|
const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId());
|
|
|
|
const shouldShowPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar");
|
|
|
|
let node = nodes[0];
|
|
|
|
while (node) {
|
|
|
|
let pillified = false;
|
|
|
|
|
|
|
|
if (node.tagName === "A" && node.getAttribute("href")) {
|
|
|
|
const href = node.getAttribute("href");
|
2021-01-27 14:46:20 +03:00
|
|
|
const parts = parseAppLocalLink(href);
|
2019-05-31 17:52:27 +03:00
|
|
|
// If the link is a (localised) matrix.to link, replace it with a pill
|
2021-01-27 14:46:20 +03:00
|
|
|
// We don't want to pill event permalinks, so those are ignored.
|
|
|
|
if (parts && !parts.eventId) {
|
2019-05-31 17:52:27 +03:00
|
|
|
const pillContainer = document.createElement('span');
|
|
|
|
|
|
|
|
const pill = <Pill
|
|
|
|
url={href}
|
|
|
|
inMessage={true}
|
|
|
|
room={room}
|
|
|
|
shouldShowPillAvatar={shouldShowPillAvatar}
|
|
|
|
/>;
|
|
|
|
|
|
|
|
ReactDOM.render(pill, pillContainer);
|
|
|
|
node.parentNode.replaceChild(pillContainer, node);
|
2020-02-23 02:51:30 +03:00
|
|
|
pills.push(pillContainer);
|
2019-05-31 17:52:27 +03:00
|
|
|
// Pills within pills aren't going to go well, so move on
|
|
|
|
pillified = true;
|
|
|
|
|
|
|
|
// update the current node with one that's now taken its place
|
|
|
|
node = pillContainer;
|
|
|
|
}
|
|
|
|
} else if (
|
|
|
|
node.nodeType === Node.TEXT_NODE &&
|
|
|
|
// as applying pills happens outside of react, make sure we're not doubly
|
|
|
|
// applying @room pills here, as a rerender with the same content won't touch the DOM
|
|
|
|
// to clear the pills from the last run of pillifyLinks
|
|
|
|
!node.parentElement.classList.contains("mx_AtRoomPill")
|
|
|
|
) {
|
|
|
|
let currentTextNode = node;
|
|
|
|
const roomNotifTextNodes = [];
|
|
|
|
|
|
|
|
// Take a textNode and break it up to make all the instances of @room their
|
|
|
|
// own textNode, adding those nodes to roomNotifTextNodes
|
|
|
|
while (currentTextNode !== null) {
|
|
|
|
const roomNotifPos = Pill.roomNotifPos(currentTextNode.textContent);
|
|
|
|
let nextTextNode = null;
|
|
|
|
if (roomNotifPos > -1) {
|
|
|
|
let roomTextNode = currentTextNode;
|
|
|
|
|
|
|
|
if (roomNotifPos > 0) roomTextNode = roomTextNode.splitText(roomNotifPos);
|
|
|
|
if (roomTextNode.textContent.length > Pill.roomNotifLen()) {
|
|
|
|
nextTextNode = roomTextNode.splitText(Pill.roomNotifLen());
|
|
|
|
}
|
|
|
|
roomNotifTextNodes.push(roomTextNode);
|
|
|
|
}
|
|
|
|
currentTextNode = nextTextNode;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (roomNotifTextNodes.length > 0) {
|
|
|
|
const pushProcessor = new PushProcessor(MatrixClientPeg.get());
|
|
|
|
const atRoomRule = pushProcessor.getPushRuleById(".m.rule.roomnotif");
|
|
|
|
if (atRoomRule && pushProcessor.ruleMatchesEvent(atRoomRule, mxEvent)) {
|
|
|
|
// Now replace all those nodes with Pills
|
|
|
|
for (const roomNotifTextNode of roomNotifTextNodes) {
|
|
|
|
// Set the next node to be processed to the one after the node
|
|
|
|
// we're adding now, since we've just inserted nodes into the structure
|
|
|
|
// we're iterating over.
|
|
|
|
// Note we've checked roomNotifTextNodes.length > 0 so we'll do this at least once
|
|
|
|
node = roomNotifTextNode.nextSibling;
|
|
|
|
|
|
|
|
const pillContainer = document.createElement('span');
|
|
|
|
const pill = <Pill
|
|
|
|
type={Pill.TYPE_AT_ROOM_MENTION}
|
|
|
|
inMessage={true}
|
|
|
|
room={room}
|
2020-07-12 21:25:43 +03:00
|
|
|
shouldShowPillAvatar={shouldShowPillAvatar}
|
2019-05-31 17:52:27 +03:00
|
|
|
/>;
|
|
|
|
|
|
|
|
ReactDOM.render(pill, pillContainer);
|
|
|
|
roomNotifTextNode.parentNode.replaceChild(pillContainer, roomNotifTextNode);
|
2020-02-23 02:51:30 +03:00
|
|
|
pills.push(pillContainer);
|
2019-05-31 17:52:27 +03:00
|
|
|
}
|
|
|
|
// Nothing else to do for a text node (and we don't need to advance
|
|
|
|
// the loop pointer because we did it above)
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (node.childNodes && node.childNodes.length && !pillified) {
|
2020-02-23 02:51:30 +03:00
|
|
|
pillifyLinks(node.childNodes, mxEvent, pills);
|
2019-05-31 17:52:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
node = node.nextSibling;
|
|
|
|
}
|
|
|
|
}
|
2020-02-23 02:51:30 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Unmount all the pill containers from React created by pillifyLinks.
|
|
|
|
*
|
|
|
|
* It's critical to call this after pillifyLinks, otherwise
|
|
|
|
* Pills will leak, leaking entire DOM trees via the event
|
|
|
|
* emitter on BaseAvatar as per
|
2020-08-03 18:02:26 +03:00
|
|
|
* https://github.com/vector-im/element-web/issues/12417
|
2020-02-23 02:51:30 +03:00
|
|
|
*
|
|
|
|
* @param {Node[]} pills - array of pill containers whose React
|
|
|
|
* components should be unmounted.
|
|
|
|
*/
|
|
|
|
export function unmountPills(pills) {
|
|
|
|
for (const pillContainer of pills) {
|
|
|
|
ReactDOM.unmountComponentAtNode(pillContainer);
|
|
|
|
}
|
|
|
|
}
|