diff --git a/res/css/_components.scss b/res/css/_components.scss
index 4986ca837f..d30684993d 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -61,6 +61,7 @@
@import "./views/dialogs/_EncryptedEventDialog.scss";
@import "./views/dialogs/_GroupAddressPicker.scss";
@import "./views/dialogs/_IncomingSasDialog.scss";
+@import "./views/dialogs/_MessageEditHistoryDialog.scss";
@import "./views/dialogs/_RestoreKeyBackupDialog.scss";
@import "./views/dialogs/_RoomSettingsDialog.scss";
@import "./views/dialogs/_RoomUpgradeDialog.scss";
diff --git a/res/css/structures/_RoomDirectory.scss b/res/css/structures/_RoomDirectory.scss
index bcfe3aefd6..1df0a61a2b 100644
--- a/res/css/structures/_RoomDirectory.scss
+++ b/res/css/structures/_RoomDirectory.scss
@@ -35,13 +35,6 @@ limitations under the License.
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 {
background-color: $button-bg-color;
border-radius: 4px;
diff --git a/res/css/views/dialogs/_MessageEditHistoryDialog.scss b/res/css/views/dialogs/_MessageEditHistoryDialog.scss
new file mode 100644
index 0000000000..b80742bd24
--- /dev/null
+++ b/res/css/views/dialogs/_MessageEditHistoryDialog.scss
@@ -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;
+ }
+}
+
diff --git a/res/css/views/messages/_MessageTimestamp.scss b/res/css/views/messages/_MessageTimestamp.scss
index e21189c59e..e5c228aa68 100644
--- a/res/css/views/messages/_MessageTimestamp.scss
+++ b/res/css/views/messages/_MessageTimestamp.scss
@@ -15,4 +15,6 @@ limitations under the License.
*/
.mx_MessageTimestamp {
+ color: $event-timestamp-color;
+ font-size: 10px;
}
diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss
index 62632eab27..1f75373be8 100644
--- a/res/css/views/rooms/_EventTile.scss
+++ b/res/css/views/rooms/_EventTile.scss
@@ -93,8 +93,6 @@ limitations under the License.
display: block;
visibility: hidden;
white-space: nowrap;
- color: $event-timestamp-color;
- font-size: 10px;
left: 0px;
width: 46px; /* 8 + 30 (avatar) + 8 */
text-align: center;
@@ -403,6 +401,7 @@ limitations under the License.
color: $roomtopic-color;
display: inline-block;
margin-left: 9px;
+ cursor: pointer;
}
/* Various markdown overrides */
diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.js b/src/components/views/dialogs/MessageEditHistoryDialog.js
new file mode 100644
index 0000000000..9d533eab56
--- /dev/null
+++ b/src/components/views/dialogs/MessageEditHistoryDialog.js
@@ -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(
);
+ }
+ nodes.push();
+ 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 = ;
+ } else {
+ const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
+ content = (
+
+ );
+ }
+ const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
+ return (
+
+ {content}
+
+ );
+ }
+}
diff --git a/src/components/views/messages/EditHistoryMessage.js b/src/components/views/messages/EditHistoryMessage.js
new file mode 100644
index 0000000000..85a704641a
--- /dev/null
+++ b/src/components/views/messages/EditHistoryMessage.js
@@ -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 = (*
+ { name }
+ {contentElements}
+
);
+ } else {
+ contentContainer = ({contentElements}
);
+ }
+ const timestamp = formatTime(new Date(mxEvent.getTs()), this.props.isTwelveHour);
+ return
+
+ {timestamp}
+ { contentContainer }
+
+ ;
+ }
+}
diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index d76956d193..25316844df 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -30,12 +30,11 @@ import Modal from '../../../Modal';
import SdkConfig from '../../../SdkConfig';
import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler';
-import MatrixClientPeg from '../../../MatrixClientPeg';
import * as ContextualMenu from '../../structures/ContextualMenu';
import SettingsStore from "../../../settings/SettingsStore";
-import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
import ReplyThread from "../elements/ReplyThread";
import {host as matrixtoHost} from '../../../matrix-to';
+import {pillifyLinks} from '../../../utils/pillify';
module.exports = React.createClass({
displayName: 'TextualBody',
@@ -99,7 +98,7 @@ module.exports = React.createClass({
// 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,
// 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);
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 = ;
-
- 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 = ;
-
- 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) {
let links = [];
@@ -454,6 +355,11 @@ module.exports = React.createClass({
this.setState({editedMarkerHovered: false});
},
+ _openHistoryDialog: async function() {
+ const MessageEditHistoryDialog = sdk.getComponent("views.dialogs.MessageEditHistoryDialog");
+ Modal.createDialog(MessageEditHistoryDialog, {mxEvent: this.props.mxEvent});
+ },
+
_renderEditedMarker: function() {
let editedTooltip;
if (this.state.editedMarkerHovered) {
@@ -462,12 +368,13 @@ module.exports = React.createClass({
const date = editEvent && formatDate(editEvent.getDate());
editedTooltip = ;
}
return (
{editedTooltip}{`(${_t("edited")})`}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index fd6da0619e..de6a06e9e4 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -954,7 +954,7 @@
"Failed to copy": "Failed to copy",
"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?",
- "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",
"Removed or unknown message type": "Removed or unknown message type",
"Message removed by %(userId)s": "Message removed by %(userId)s",
@@ -1199,6 +1199,7 @@
"Manually export keys": "Manually export keys",
"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?",
+ "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.",
"To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.",
"Report bugs & give feedback": "Report bugs & give feedback",
diff --git a/src/utils/pillify.js b/src/utils/pillify.js
new file mode 100644
index 0000000000..e943cfe657
--- /dev/null
+++ b/src/utils/pillify.js
@@ -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 = ;
+
+ 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 = ;
+
+ 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;
+ }
+}