Merge pull request #3144 from matrix-org/bwindels/edit-history
Edit history dialog
This commit is contained in:
commit
15d286ed93
10 changed files with 342 additions and 112 deletions
|
@ -61,6 +61,7 @@
|
||||||
@import "./views/dialogs/_EncryptedEventDialog.scss";
|
@import "./views/dialogs/_EncryptedEventDialog.scss";
|
||||||
@import "./views/dialogs/_GroupAddressPicker.scss";
|
@import "./views/dialogs/_GroupAddressPicker.scss";
|
||||||
@import "./views/dialogs/_IncomingSasDialog.scss";
|
@import "./views/dialogs/_IncomingSasDialog.scss";
|
||||||
|
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
|
||||||
@import "./views/dialogs/_RestoreKeyBackupDialog.scss";
|
@import "./views/dialogs/_RestoreKeyBackupDialog.scss";
|
||||||
@import "./views/dialogs/_RoomSettingsDialog.scss";
|
@import "./views/dialogs/_RoomSettingsDialog.scss";
|
||||||
@import "./views/dialogs/_RoomUpgradeDialog.scss";
|
@import "./views/dialogs/_RoomUpgradeDialog.scss";
|
||||||
|
|
|
@ -35,13 +35,6 @@ limitations under the License.
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomDirectory .gm-scroll-view {
|
|
||||||
// little hack because gemini doesn't seem to detect
|
|
||||||
// the scrollbar width well in this instance
|
|
||||||
// when using css scrollbars
|
|
||||||
scrollbar-width: thin;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomDirectory_createRoom {
|
.mx_RoomDirectory_createRoom {
|
||||||
background-color: $button-bg-color;
|
background-color: $button-bg-color;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
41
res/css/views/dialogs/_MessageEditHistoryDialog.scss
Normal file
41
res/css/views/dialogs/_MessageEditHistoryDialog.scss
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_MessageEditHistoryDialog .mx_Dialog_header > .mx_Dialog_title {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageEditHistoryDialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 60vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageEditHistoryDialog_scrollPanel {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageEditHistoryDialog_edits {
|
||||||
|
list-style-type: none;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
|
||||||
|
.mx_EventTile_line, .mx_EventTile_content {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -15,4 +15,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_MessageTimestamp {
|
.mx_MessageTimestamp {
|
||||||
|
color: $event-timestamp-color;
|
||||||
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,8 +93,6 @@ limitations under the License.
|
||||||
display: block;
|
display: block;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: $event-timestamp-color;
|
|
||||||
font-size: 10px;
|
|
||||||
left: 0px;
|
left: 0px;
|
||||||
width: 46px; /* 8 + 30 (avatar) + 8 */
|
width: 46px; /* 8 + 30 (avatar) + 8 */
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -403,6 +401,7 @@ limitations under the License.
|
||||||
color: $roomtopic-color;
|
color: $roomtopic-color;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 9px;
|
margin-left: 9px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Various markdown overrides */
|
/* Various markdown overrides */
|
||||||
|
|
108
src/components/views/dialogs/MessageEditHistoryDialog.js
Normal file
108
src/components/views/dialogs/MessageEditHistoryDialog.js
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 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 from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import sdk from "../../../index";
|
||||||
|
import {wantsDateSeparator} from '../../../DateUtils';
|
||||||
|
import SettingsStore from '../../../settings/SettingsStore';
|
||||||
|
|
||||||
|
export default class MessageEditHistoryDialog extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
mxEvent: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
events: [],
|
||||||
|
nextBatch: null,
|
||||||
|
isLoading: true,
|
||||||
|
isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMoreEdits = async (backwards) => {
|
||||||
|
if (backwards || (!this.state.nextBatch && !this.state.isLoading)) {
|
||||||
|
// bail out on backwards as we only paginate in one direction
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const opts = {from: this.state.nextBatch};
|
||||||
|
const roomId = this.props.mxEvent.getRoomId();
|
||||||
|
const eventId = this.props.mxEvent.getId();
|
||||||
|
const result = await MatrixClientPeg.get().relations(
|
||||||
|
roomId, eventId, "m.replace", "m.room.message", opts);
|
||||||
|
let resolve;
|
||||||
|
const promise = new Promise(r => resolve = r);
|
||||||
|
this.setState({
|
||||||
|
events: this.state.events.concat(result.events),
|
||||||
|
nextBatch: result.nextBatch,
|
||||||
|
isLoading: false,
|
||||||
|
}, () => {
|
||||||
|
const hasMoreResults = !!this.state.nextBatch;
|
||||||
|
resolve(hasMoreResults);
|
||||||
|
});
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.loadMoreEdits();
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderEdits() {
|
||||||
|
const EditHistoryMessage = sdk.getComponent('messages.EditHistoryMessage');
|
||||||
|
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||||
|
const nodes = [];
|
||||||
|
let lastEvent;
|
||||||
|
this.state.events.forEach(e => {
|
||||||
|
if (!lastEvent || wantsDateSeparator(lastEvent.getDate(), e.getDate())) {
|
||||||
|
nodes.push(<li key={e.getTs() + "~"}><DateSeparator ts={e.getTs()} /></li>);
|
||||||
|
}
|
||||||
|
nodes.push(<EditHistoryMessage key={e.getId()} mxEvent={e} isTwelveHour={this.state.isTwelveHour} />);
|
||||||
|
lastEvent = e;
|
||||||
|
});
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let content;
|
||||||
|
if (this.state.error) {
|
||||||
|
content = this.state.error;
|
||||||
|
} else if (this.state.isLoading) {
|
||||||
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
content = <Spinner />;
|
||||||
|
} else {
|
||||||
|
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||||
|
content = (<ScrollPanel
|
||||||
|
className="mx_MessageEditHistoryDialog_scrollPanel"
|
||||||
|
onFillRequest={ this.loadMoreEdits }
|
||||||
|
stickyBottom={false}
|
||||||
|
startAtBottom={false}
|
||||||
|
>
|
||||||
|
<ul className="mx_MessageEditHistoryDialog_edits mx_MessagePanel_alwaysShowTimestamps">{this._renderEdits()}</ul>
|
||||||
|
</ScrollPanel>);
|
||||||
|
}
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
return (
|
||||||
|
<BaseDialog className='mx_MessageEditHistoryDialog' hasCancel={true}
|
||||||
|
onFinished={this.props.onFinished} title={_t("Message edits")}>
|
||||||
|
{content}
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
60
src/components/views/messages/EditHistoryMessage.js
Normal file
60
src/components/views/messages/EditHistoryMessage.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 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 from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import * as HtmlUtils from '../../../HtmlUtils';
|
||||||
|
import {formatTime} from '../../../DateUtils';
|
||||||
|
import {MatrixEvent} from 'matrix-js-sdk';
|
||||||
|
import {pillifyLinks} from '../../../utils/pillify';
|
||||||
|
|
||||||
|
export default class EditHistoryMessage extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
// the message event being edited
|
||||||
|
mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
pillifyLinks(this.refs.content.children, this.props.mxEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
pillifyLinks(this.refs.content.children, this.props.mxEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {mxEvent} = this.props;
|
||||||
|
const content = mxEvent.event.content["m.new_content"] || mxEvent.event.content;
|
||||||
|
const contentElements = HtmlUtils.bodyToHtml(content);
|
||||||
|
let contentContainer;
|
||||||
|
if (mxEvent.getContent().msgtype === "m.emote") {
|
||||||
|
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||||
|
contentContainer = (<div className="mx_EventTile_content" ref="content">*
|
||||||
|
<span className="mx_MEmoteBody_sender">{ name }</span>
|
||||||
|
{contentElements}
|
||||||
|
</div>);
|
||||||
|
} else {
|
||||||
|
contentContainer = (<div className="mx_EventTile_content" ref="content">{contentElements}</div>);
|
||||||
|
}
|
||||||
|
const timestamp = formatTime(new Date(mxEvent.getTs()), this.props.isTwelveHour);
|
||||||
|
return <li className="mx_EventTile">
|
||||||
|
<div className="mx_EventTile_line">
|
||||||
|
<span className="mx_MessageTimestamp">{timestamp}</span>
|
||||||
|
{ contentContainer }
|
||||||
|
</div>
|
||||||
|
</li>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,12 +30,11 @@ import Modal from '../../../Modal';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
|
||||||
import * as ContextualMenu from '../../structures/ContextualMenu';
|
import * as ContextualMenu from '../../structures/ContextualMenu';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
|
||||||
import ReplyThread from "../elements/ReplyThread";
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
import {host as matrixtoHost} from '../../../matrix-to';
|
import {host as matrixtoHost} from '../../../matrix-to';
|
||||||
|
import {pillifyLinks} from '../../../utils/pillify';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'TextualBody',
|
displayName: 'TextualBody',
|
||||||
|
@ -99,7 +98,7 @@ module.exports = React.createClass({
|
||||||
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
|
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
|
||||||
// are still sent as plaintext URLs. If these are ever pillified in the composer,
|
// are still sent as plaintext URLs. If these are ever pillified in the composer,
|
||||||
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
|
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
|
||||||
this.pillifyLinks(this.refs.content.children);
|
pillifyLinks(this.refs.content.children, this.props.mxEvent);
|
||||||
HtmlUtils.linkifyElement(this.refs.content);
|
HtmlUtils.linkifyElement(this.refs.content);
|
||||||
this.calculateUrlPreview();
|
this.calculateUrlPreview();
|
||||||
|
|
||||||
|
@ -184,104 +183,6 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
pillifyLinks: function(nodes) {
|
|
||||||
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");
|
|
||||||
|
|
||||||
// If the link is a (localised) matrix.to link, replace it with a pill
|
|
||||||
const Pill = sdk.getComponent('elements.Pill');
|
|
||||||
if (Pill.isMessagePillUrl(href)) {
|
|
||||||
const pillContainer = document.createElement('span');
|
|
||||||
|
|
||||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
|
||||||
const pill = <Pill
|
|
||||||
url={href}
|
|
||||||
inMessage={true}
|
|
||||||
room={room}
|
|
||||||
shouldShowPillAvatar={shouldShowPillAvatar}
|
|
||||||
/>;
|
|
||||||
|
|
||||||
ReactDOM.render(pill, pillContainer);
|
|
||||||
node.parentNode.replaceChild(pillContainer, node);
|
|
||||||
// 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")
|
|
||||||
) {
|
|
||||||
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 (atRoomRule && pushProcessor.ruleMatchesEvent(atRoomRule, this.props.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 room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
|
||||||
const pill = <Pill
|
|
||||||
type={Pill.TYPE_AT_ROOM_MENTION}
|
|
||||||
inMessage={true}
|
|
||||||
room={room}
|
|
||||||
shouldShowPillAvatar={true}
|
|
||||||
/>;
|
|
||||||
|
|
||||||
ReactDOM.render(pill, pillContainer);
|
|
||||||
roomNotifTextNode.parentNode.replaceChild(pillContainer, roomNotifTextNode);
|
|
||||||
}
|
|
||||||
// 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) {
|
|
||||||
this.pillifyLinks(node.childNodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
node = node.nextSibling;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
findLinks: function(nodes) {
|
findLinks: function(nodes) {
|
||||||
let links = [];
|
let links = [];
|
||||||
|
|
||||||
|
@ -454,6 +355,11 @@ module.exports = React.createClass({
|
||||||
this.setState({editedMarkerHovered: false});
|
this.setState({editedMarkerHovered: false});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_openHistoryDialog: async function() {
|
||||||
|
const MessageEditHistoryDialog = sdk.getComponent("views.dialogs.MessageEditHistoryDialog");
|
||||||
|
Modal.createDialog(MessageEditHistoryDialog, {mxEvent: this.props.mxEvent});
|
||||||
|
},
|
||||||
|
|
||||||
_renderEditedMarker: function() {
|
_renderEditedMarker: function() {
|
||||||
let editedTooltip;
|
let editedTooltip;
|
||||||
if (this.state.editedMarkerHovered) {
|
if (this.state.editedMarkerHovered) {
|
||||||
|
@ -462,12 +368,13 @@ module.exports = React.createClass({
|
||||||
const date = editEvent && formatDate(editEvent.getDate());
|
const date = editEvent && formatDate(editEvent.getDate());
|
||||||
editedTooltip = <Tooltip
|
editedTooltip = <Tooltip
|
||||||
tooltipClassName="mx_Tooltip_timeline"
|
tooltipClassName="mx_Tooltip_timeline"
|
||||||
label={_t("Edited at %(date)s", {date})}
|
label={_t("Edited at %(date)s. Click to view edits.", {date})}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key="editedMarker" className="mx_EventTile_edited"
|
key="editedMarker" className="mx_EventTile_edited"
|
||||||
|
onClick={this._openHistoryDialog}
|
||||||
onMouseEnter={this._onMouseEnterEditedMarker}
|
onMouseEnter={this._onMouseEnterEditedMarker}
|
||||||
onMouseLeave={this._onMouseLeaveEditedMarker}
|
onMouseLeave={this._onMouseLeaveEditedMarker}
|
||||||
>{editedTooltip}<span>{`(${_t("edited")})`}</span></div>
|
>{editedTooltip}<span>{`(${_t("edited")})`}</span></div>
|
||||||
|
|
|
@ -954,7 +954,7 @@
|
||||||
"Failed to copy": "Failed to copy",
|
"Failed to copy": "Failed to copy",
|
||||||
"Add an Integration": "Add an Integration",
|
"Add an Integration": "Add an Integration",
|
||||||
"You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?",
|
"You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?",
|
||||||
"Edited at %(date)s": "Edited at %(date)s",
|
"Edited at %(date)s. Click to view edits.": "Edited at %(date)s. Click to view edits.",
|
||||||
"edited": "edited",
|
"edited": "edited",
|
||||||
"Removed or unknown message type": "Removed or unknown message type",
|
"Removed or unknown message type": "Removed or unknown message type",
|
||||||
"Message removed by %(userId)s": "Message removed by %(userId)s",
|
"Message removed by %(userId)s": "Message removed by %(userId)s",
|
||||||
|
@ -1199,6 +1199,7 @@
|
||||||
"Manually export keys": "Manually export keys",
|
"Manually export keys": "Manually export keys",
|
||||||
"You'll lose access to your encrypted messages": "You'll lose access to your encrypted messages",
|
"You'll lose access to your encrypted messages": "You'll lose access to your encrypted messages",
|
||||||
"Are you sure you want to sign out?": "Are you sure you want to sign out?",
|
"Are you sure you want to sign out?": "Are you sure you want to sign out?",
|
||||||
|
"Message edits": "Message edits",
|
||||||
"If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.",
|
"If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.",
|
||||||
"To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.": "To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.",
|
"To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.": "To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.",
|
||||||
"Report bugs & give feedback": "Report bugs & give feedback",
|
"Report bugs & give feedback": "Report bugs & give feedback",
|
||||||
|
|
118
src/utils/pillify.js
Normal file
118
src/utils/pillify.js
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 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 ReactDOM from 'react-dom';
|
||||||
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
|
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
||||||
|
import sdk from '../index';
|
||||||
|
|
||||||
|
export function pillifyLinks(nodes, mxEvent) {
|
||||||
|
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");
|
||||||
|
|
||||||
|
// If the link is a (localised) matrix.to link, replace it with a pill
|
||||||
|
const Pill = sdk.getComponent('elements.Pill');
|
||||||
|
if (Pill.isMessagePillUrl(href)) {
|
||||||
|
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);
|
||||||
|
// 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")
|
||||||
|
) {
|
||||||
|
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 (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}
|
||||||
|
shouldShowPillAvatar={true}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
ReactDOM.render(pill, pillContainer);
|
||||||
|
roomNotifTextNode.parentNode.replaceChild(pillContainer, roomNotifTextNode);
|
||||||
|
}
|
||||||
|
// 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) {
|
||||||
|
pillifyLinks(node.childNodes, mxEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
node = node.nextSibling;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue