From 4f8d6d8fbed01df0c18518401a7c42243462dd11 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 1 Nov 2017 19:42:47 +0000 Subject: [PATCH] Pillify room notifs in the timeline This scans text nodes in the DOM for room notifications and turns them into pills. Changes the pillification code around a bit so it works with text nodes. Uses the push processor directly to test the event against the room notifiation rule so we know whether this event would actually trigger a room notification (needs to hook into push at a lower level because otherwise our own room notifications would not pillify since our own events never generate notifications). Requires https://github.com/matrix-org/matrix-js-sdk/pull/565 --- src/components/views/elements/Pill.js | 42 ++++++++++-- src/components/views/messages/TextualBody.js | 69 ++++++++++++++++++-- 2 files changed, 101 insertions(+), 10 deletions(-) diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js index 51ae85ba5a..a85f83d78c 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.js @@ -37,11 +37,20 @@ const Pill = React.createClass({ isMessagePillUrl: (url) => { return !!REGEX_LOCAL_MATRIXTO.exec(url); }, + roomNotifPos: (text) => { + return text.indexOf("@room"); + }, + roomNotifLen: () => { + return "@room".length; + }, TYPE_USER_MENTION: 'TYPE_USER_MENTION', TYPE_ROOM_MENTION: 'TYPE_ROOM_MENTION', + TYPE_AT_ROOM_MENTION: 'TYPE_AT_ROOM_MENTION', // '@room' mention }, props: { + // The Type of this Pill. If url is given, this is auto-detected. + type: PropTypes.string, // The URL to pillify (no validation is done, see isPillUrl and isMessagePillUrl) url: PropTypes.string, // Whether the pill is in a message @@ -72,14 +81,20 @@ const Pill = React.createClass({ regex = REGEX_LOCAL_MATRIXTO; } - // Default to the empty array if no match for simplicity - // resource and prefix will be undefined instead of throwing - const matrixToMatch = regex.exec(nextProps.url) || []; + let matrixToMatch; + let resourceId; + let prefix; - const resourceId = matrixToMatch[1]; // The room/user ID - const prefix = matrixToMatch[2]; // The first character of prefix + if (nextProps.url) { + // Default to the empty array if no match for simplicity + // resource and prefix will be undefined instead of throwing + matrixToMatch = regex.exec(nextProps.url) || []; - const pillType = { + resourceId = matrixToMatch[1]; // The room/user ID + prefix = matrixToMatch[2]; // The first character of prefix + } + + const pillType = this.props.type || { '@': Pill.TYPE_USER_MENTION, '#': Pill.TYPE_ROOM_MENTION, '!': Pill.TYPE_ROOM_MENTION, @@ -88,6 +103,10 @@ const Pill = React.createClass({ let member; let room; switch (pillType) { + case Pill.TYPE_AT_ROOM_MENTION: { + room = nextProps.room; + } + break; case Pill.TYPE_USER_MENTION: { const localMember = nextProps.room.getMember(resourceId); member = localMember; @@ -160,6 +179,17 @@ const Pill = React.createClass({ let href = this.props.url; let onClick; switch (this.state.pillType) { + case Pill.TYPE_AT_ROOM_MENTION: { + const room = this.props.room; + if (room) { + linkText = "@room"; + if (this.props.shouldShowPillAvatar) { + avatar = ; + } + pillClass = 'mx_AtRoomPill'; + } + } + break; case Pill.TYPE_USER_MENTION: { // If this user is not a member of this room, default to the empty member const member = this.state.member; diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 64b23238e5..faa4d6cf77 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -34,6 +34,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; import ContextualMenu from '../../structures/ContextualMenu'; import {RoomMember} from 'matrix-js-sdk'; import classNames from 'classnames'; +import PushProcessor from 'matrix-js-sdk/lib/pushprocessor'; linkifyMatrix(linkify); @@ -169,8 +170,10 @@ module.exports = React.createClass({ pillifyLinks: function(nodes) { const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false); - for (let i = 0; i < nodes.length; i++) { - const node = nodes[i]; + let node = nodes[0]; + while (node) { + let pillified = false; + if (node.tagName === "A" && node.getAttribute("href")) { const href = node.getAttribute("href"); @@ -189,10 +192,68 @@ module.exports = React.createClass({ ReactDOM.render(pill, pillContainer); node.parentNode.replaceChild(pillContainer, node); + // Pills within pills aren't going to go well, so move on + pillified = true; + } + } else if (node.nodeType == Node.TEXT_NODE) { + const Pill = sdk.getComponent('elements.Pill'); + + 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 (pushProcessor.ruleMatchesEvent(atRoomRule, this.props.mxEvent)) { + // Now replace all those nodes with Pills + for (const roomNotifTextNode of roomNotifTextNodes) { + const pillContainer = document.createElement('span'); + const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); + const pill = ; + + ReactDOM.render(pill, pillContainer); + roomNotifTextNode.parentNode.replaceChild(pillContainer, roomNotifTextNode); + + // 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; + } + // Nothing else to do for a text node (and we don't need to advance + // the loop pointer because we did it above) + continue; + } } - } else if (node.children && node.children.length) { - this.pillifyLinks(node.children); } + + if (node.childNodes && node.childNodes.length && !pillified) { + this.pillifyLinks(node.childNodes); + } + + node = node.nextSibling; } },