Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Copy events from the timeline in an IRC log style format #3370

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 145 additions & 1 deletion src/components/structures/RoomView.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import ObjectUtils from '../../ObjectUtils';
import * as Rooms from '../../Rooms';

import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
import { formatDate } from '../../DateUtils';

import MainSplit from './MainSplit';
import RightPanel from './RightPanel';
Expand Down Expand Up @@ -1449,6 +1450,149 @@ module.exports = React.createClass({
this.setState({auxPanelMaxHeight: auxPanelMaxHeight});
},

_findEventTileContent: (node) => {
while (node) {
if (node.classList && node.classList.contains("mx_RoomView_MessageList")) {
return null;
}
if (node.classList && node.classList.contains("mx_Content")) {
return node;
}
node = node.parentNode;
}
return null;
},

_findListItem: (node) => {
while (node.parentNode) {
if (node.parentNode.classList &&
node.parentNode.classList.contains("mx_RoomView_MessageList"))
{
return node;
}
node = node.parentNode;
}
return null;
},

_pruneStart: function(node, startNode, startOffset) {
// todo: prune contents before the selection start offset
return node;
},

_pruneEnd: function(node, endNode, endOffset) {
// todo: prune contents after the selection end offset
return node;
},

_formatNodeForCopy: function(node, eventId) {
let html, text;
const mxEvent = this.state.room.findEventById(eventId);
const sender = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
const showTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
const ts = formatDate(new Date(mxEvent.getTs()), showTwelveHour);
// FIXME handle replies properly
const contents = node.querySelectorAll(".mx_EventTile_line > .mx_Content");
const content = contents[contents.length - 1];
if (content) {
// FIXME: replace this with rendering from EventTiles once we add the ability
// to render out events in 'log' mode.
if (mxEvent.getType() === 'm.room.message' || mxEvent.getType() === 'm.room.sticker') {
html = `<div>[${ts}] &lt;${sender}&gt; ${content.innerHTML}</div>\n`;
text = `[${ts}] <${sender}> ${content.innerText}\n`;
}
else {
html = `<div>[${ts}] ${content.innerHTML}</div>\n`;
text = `[${ts}] ${content.innerText}\n`;
}
}
else {
html = node.innerHTML;
text = node.innerText;
}

return { html, text };
},

onCopy: function(ev) {
const sel = document.getSelection();
// iterate over all the selected events and build up pretty
// plaintext & HTML representations of them

// if we're copying a fragment of content then don't do anything funky
if (sel.anchorNode === sel.focusNode ||
(this._findEventTileContent(sel.anchorNode) === this._findEventTileContent(sel.focusNode) &&
this._findEventTileContent(sel.anchorNode) != null))
{
console.log("defaulting to normal copy as we think we're in the same content");
return;
}

// alternatively: find which eventtile the start is in, and the end is in, and then iterate
// over rendering out the EventTiles in 'logging' mode (whatever that turns out best to be),
// and then plonk that in the clipboard. Unclear how to handle CSS for that.
// we give up on handling the start/end offsets for now.

// otherwise, we must be spanning multiple nodes, so let's prepend
// nice timestamp and sender info. in order to respect the offsets of the anchor
// and focus end of the selection block, we do this by traversing the selection DOM,
// rebuilding the metadata blocks but keeping the data blocks intact.

const dir = sel.anchorNode.compareDocumentPosition(sel.focusNode);
const startNode = dir & Node.DOCUMENT_POSITION_PRECEDING ? sel.focusNode : sel.anchorNode;
const endNode = dir & Node.DOCUMENT_POSITION_PRECEDING ? sel.anchorNode : sel.focusNode;
const startOffset = dir & Node.DOCUMENT_POSITION_PRECEDING ? sel.focusOffset : sel.anchorOffset;
const endOffset = dir & Node.DOCUMENT_POSITION_PRECEDING ? sel.anchorOffset : sel.focusOffset;

const messageList = ReactDOM.findDOMNode(this.refs.messagePanel).querySelector(".mx_RoomView_MessageList");
const startItem = this._findListItem(startNode) || messageList.firstChild;
const endItem = this._findListItem(endNode) || messageList.lastChild;

let html = "";
let text = "";
for (let item = startItem; item !== endItem; item = item.nextElementSibling) {
let node = item;

if (item === startItem) {
node = this._pruneStart(item, startNode, startOffset);
}
else if (item === endItem) {
node = this._pruneEnd(item, endNode, endOffset);
}

const eventId = node.getAttribute("data-scroll-tokens");
if (eventId && this.state.room.findEventById(eventId)) {
const result = this._formatNodeForCopy(node, eventId);
html += result.html;
text += result.text;
}
else if (node.classList.contains("mx_MemberEventListSummary")) {
for (let melItem = node.firstChild; melItem; melItem = melItem.nextElementSibling) {
console.log("melItem", melItem.innerHTML);
const melEventId = melItem.getAttribute("data-scroll-tokens");
if (melEventId && this.state.room.findEventById(melEventId)) {
const result = this._formatNodeForCopy(melItem, melEventId);
html += result.html;
text += result.text;
}
}
}
else {
if (node.querySelector(".mx_DateSeparator")) continue;
// fall back for MELS etc
html += node.innerHTML;
text += node.innerText;
}
}

console.log("setting text clipboard to: ", text);
console.log("setting html clipboard to: ", html);

ev.clipboardData.setData('text/plain', text);
ev.clipboardData.setData('text/html', html);
ev.preventDefault();
},

onFullscreenClick: function() {
dis.dispatch({
action: 'video_fullscreen',
Expand Down Expand Up @@ -1955,7 +2099,7 @@ module.exports = React.createClass({
>
<div className={fadableSectionClasses}>
{ auxPanel }
<div className="mx_RoomView_timeline">
<div className="mx_RoomView_timeline" onCopy={ this.onCopy }>
{ topUnreadMessagesBar }
{ jumpToBottom }
{ messagePanel }
Expand Down
6 changes: 3 additions & 3 deletions src/components/views/messages/MAudioBody.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export default class MAudioBody extends React.Component {

if (this.state.error !== null) {
return (
<span className="mx_MAudioBody" ref="body">
<span className="mx_MAudioBody mx_Content" ref="body">
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
{ _t("Error decrypting audio") }
</span>
Expand All @@ -93,7 +93,7 @@ export default class MAudioBody extends React.Component {
// For now add an img tag with a 16x16 spinner.
// Not sure how tall the audio player is so not sure how tall it should actually be.
return (
<span className="mx_MAudioBody">
<span className="mx_MAudioBody mx_Content">
<img src={require("../../../../res/img/spinner.gif")} alt={content.body} width="16" height="16" />
</span>
);
Expand All @@ -102,7 +102,7 @@ export default class MAudioBody extends React.Component {
const contentUrl = this._getContentUrl();

return (
<span className="mx_MAudioBody">
<span className="mx_MAudioBody mx_Content">
<audio src={contentUrl} controls />
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
</span>
Expand Down
10 changes: 5 additions & 5 deletions src/components/views/messages/MFileBody.js
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ module.exports = React.createClass({
};

return (
<span className="mx_MFileBody" ref="body">
<span className="mx_MFileBody mx_Content" ref="body">
<div className="mx_MFileBody_download">
<a href="javascript:void(0)" onClick={decrypt}>
{ _t("Decrypt %(text)s", { text: text }) }
Expand Down Expand Up @@ -360,7 +360,7 @@ module.exports = React.createClass({
}
renderer_url += "?origin=" + encodeURIComponent(window.location.origin);
return (
<span className="mx_MFileBody">
<span className="mx_MFileBody mx_Content">
<div className="mx_MFileBody_download">
<div style={{display: "none"}}>
{ /*
Expand Down Expand Up @@ -424,7 +424,7 @@ module.exports = React.createClass({
// files in the right hand side of the screen.
if (this.props.tileShape === "file_grid") {
return (
<span className="mx_MFileBody">
<span className="mx_MFileBody mx_Content">
<div className="mx_MFileBody_download">
<a className="mx_MFileBody_downloadLink" {...downloadProps}>
{ fileName }
Expand All @@ -437,7 +437,7 @@ module.exports = React.createClass({
);
} else {
return (
<span className="mx_MFileBody">
<span className="mx_MFileBody mx_Content">
<div className="mx_MFileBody_download">
<a {...downloadProps}>
<img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage" />
Expand All @@ -449,7 +449,7 @@ module.exports = React.createClass({
}
} else {
const extra = text ? (': ' + text) : '';
return <span className="mx_MFileBody">
return <span className="mx_MFileBody mx_Content">
{ _t("Invalid file%(extra)s", { extra: extra }) }
</span>;
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/views/messages/MImageBody.js
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ export default class MImageBody extends React.Component {

if (this.state.error !== null) {
return (
<span className="mx_MImageBody" ref="body">
<span className="mx_MImageBody mx_Content" ref="body">
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
{ _t("Error decrypting image") }
</span>
Expand All @@ -448,7 +448,7 @@ export default class MImageBody extends React.Component {
const thumbnail = this._messageContent(contentUrl, thumbUrl, content);
const fileBody = this.getFileBody();

return <span className="mx_MImageBody" ref="body">
return <span className="mx_MImageBody mx_Content" ref="body">
{ thumbnail }
{ fileBody }
</span>;
Expand Down
6 changes: 3 additions & 3 deletions src/components/views/messages/MVideoBody.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ module.exports = React.createClass({

if (this.state.error !== null) {
return (
<span className="mx_MVideoBody" ref="body">
<span className="mx_MVideoBody mx_Content" ref="body">
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
{ _t("Error decrypting video") }
</span>
Expand All @@ -146,7 +146,7 @@ module.exports = React.createClass({
// The attachment is decrypted in componentDidMount.
// For now add an img tag with a spinner.
return (
<span className="mx_MVideoBody" ref="body">
<span className="mx_MVideoBody mx_Content" ref="body">
<div className="mx_MImageBody_thumbnail mx_MImageBody_thumbnail_spinner" ref="image">
<img src={require("../../../../res/img/spinner.gif")} alt={content.body} width="16" height="16" />
</div>
Expand Down Expand Up @@ -174,7 +174,7 @@ module.exports = React.createClass({
}
}
return (
<span className="mx_MVideoBody">
<span className="mx_MVideoBody mx_Content">
<video className="mx_MVideoBody" src={contentUrl} alt={content.body}
controls preload={preload} muted={autoplay} autoPlay={autoplay}
height={height} width={width} poster={poster}>
Expand Down
4 changes: 2 additions & 2 deletions src/components/views/messages/RoomAvatarEvent.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ module.exports = React.createClass({

if (!ev.getContent().url || ev.getContent().url.trim().length === 0) {
return (
<div className="mx_TextualEvent">
<div className="mx_TextualEvent mx_Content">
{ _t('%(senderDisplayName)s removed the room avatar.', {senderDisplayName}) }
</div>
);
Expand All @@ -71,7 +71,7 @@ module.exports = React.createClass({
};

return (
<div className="mx_RoomAvatarEvent">
<div className="mx_RoomAvatarEvent mx_Content">
{ _t('%(senderDisplayName)s changed the room avatar to <img/>',
{ senderDisplayName: senderDisplayName },
{
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/messages/RoomCreate.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ module.exports = React.createClass({
const permalinkCreator = new RoomPermalinkCreator(prevRoom, predecessor['room_id']);
permalinkCreator.load();
const predecessorPermalink = permalinkCreator.forEvent(predecessor['event_id']);
return <div className="mx_CreateEvent">
return <div className="mx_CreateEvent mx_Content">
<div className="mx_CreateEvent_image" />
<div className="mx_CreateEvent_header">
{_t("This room is a continuation of another conversation.")}
Expand Down
6 changes: 3 additions & 3 deletions src/components/views/messages/TextualBody.js
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ module.exports = React.createClass({
case "m.emote":
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
return (
<span ref="content" className="mx_MEmoteBody mx_EventTile_content">
<span ref="content" className="mx_MEmoteBody mx_EventTile_content mx_Content">
*&nbsp;
<span
className="mx_MEmoteBody_sender"
Expand All @@ -474,14 +474,14 @@ module.exports = React.createClass({
);
case "m.notice":
return (
<span ref="content" className="mx_MNoticeBody mx_EventTile_content">
<span ref="content" className="mx_MNoticeBody mx_EventTile_content mx_Content">
{ body }
{ widgets }
</span>
);
default: // including "m.text"
return (
<span ref="content" className="mx_MTextBody mx_EventTile_content">
<span ref="content" className="mx_MTextBody mx_EventTile_content mx_Content">
{ body }
{ widgets }
</span>
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/messages/TextualEvent.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ module.exports = React.createClass({
const text = TextForEvent.textForEvent(this.props.mxEvent);
if (text == null || text.length === 0) return null;
return (
<div className="mx_TextualEvent">{ text }</div>
<div className="mx_TextualEvent mx_Content">{ text }</div>
);
},
});
2 changes: 1 addition & 1 deletion src/components/views/messages/UnknownBody.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ module.exports = React.createClass({

const text = this.props.mxEvent.getContent().body;
return (
<span className="mx_UnknownBody" title={tooltip}>
<span className="mx_UnknownBody mx_Content" title={tooltip}>
{ text }
</span>
);
Expand Down