make quoting work

pull/21833/head
Matthew Hodgson 2018-05-21 03:48:59 +01:00
parent 4439a04689
commit 7de45f8b7b
3 changed files with 85 additions and 56 deletions

View File

@ -403,19 +403,22 @@ class TextHighlighter extends BaseHighlighter {
} }
/* turn a matrix event body into html /* turn a matrix event body into html
* *
* content: 'content' of the MatrixEvent * content: 'content' of the MatrixEvent
* *
* highlights: optional list of words to highlight, ordered by longest word first * highlights: optional list of words to highlight, ordered by longest word first
* *
* opts.highlightLink: optional href to add to highlighted words * opts.highlightLink: optional href to add to highlighted words
* opts.disableBigEmoji: optional argument to disable the big emoji class. * opts.disableBigEmoji: optional argument to disable the big emoji class.
* opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing * opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing
*/ * opts.returnString: return an HTML string rather than JSX elements
* opts.emojiOne: optional param to do emojiOne (default true)
*/
export function bodyToHtml(content, highlights, opts={}) { export function bodyToHtml(content, highlights, opts={}) {
const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body; const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body;
const doEmojiOne = opts.emojiOne === undefined ? true : opts.emojiOne;
let bodyHasEmoji = false; let bodyHasEmoji = false;
let strippedBody; let strippedBody;
@ -441,8 +444,9 @@ export function bodyToHtml(content, highlights, opts={}) {
if (opts.stripReplyFallback && formattedBody) formattedBody = ReplyThread.stripHTMLReply(formattedBody); if (opts.stripReplyFallback && formattedBody) formattedBody = ReplyThread.stripHTMLReply(formattedBody);
strippedBody = opts.stripReplyFallback ? ReplyThread.stripPlainReply(content.body) : content.body; strippedBody = opts.stripReplyFallback ? ReplyThread.stripPlainReply(content.body) : content.body;
bodyHasEmoji = containsEmoji(isHtmlMessage ? formattedBody : content.body); if (doEmojiOne) {
bodyHasEmoji = containsEmoji(isHtmlMessage ? formattedBody : content.body);
}
// Only generate safeBody if the message was sent as org.matrix.custom.html // Only generate safeBody if the message was sent as org.matrix.custom.html
if (isHtmlMessage) { if (isHtmlMessage) {
@ -467,6 +471,10 @@ export function bodyToHtml(content, highlights, opts={}) {
delete sanitizeHtmlParams.textFilter; delete sanitizeHtmlParams.textFilter;
} }
if (opts.returnString) {
return isDisplayedWithHtml ? safeBody : strippedBody;
}
let emojiBody = false; let emojiBody = false;
if (!opts.disableBigEmoji && bodyHasEmoji) { if (!opts.disableBigEmoji && bodyHasEmoji) {
EMOJI_REGEX.lastIndex = 0; EMOJI_REGEX.lastIndex = 0;

View File

@ -179,7 +179,7 @@ module.exports = React.createClass({
onQuoteClick: function() { onQuoteClick: function() {
dis.dispatch({ dis.dispatch({
action: 'quote', action: 'quote',
text: this.props.eventTileOps.getInnerText(), event: this.props.mxEvent,
}); });
this.closeMenu(); this.closeMenu();
}, },

View File

@ -21,7 +21,7 @@ import type SyntheticKeyboardEvent from 'react/lib/SyntheticKeyboardEvent';
import { Editor } from 'slate-react'; import { Editor } from 'slate-react';
import { getEventTransfer } from 'slate-react'; import { getEventTransfer } from 'slate-react';
import { Value, Document, Event, Inline, Text, Range, Node } from 'slate'; import { Value, Document, Event, Block, Inline, Text, Range, Node } from 'slate';
import Html from 'slate-html-serializer'; import Html from 'slate-html-serializer';
import Md from 'slate-md-serializer'; import Md from 'slate-md-serializer';
@ -342,37 +342,44 @@ export default class MessageComposerInput extends React.Component {
}); });
} }
break; break;
/* case 'quote': {
case 'quote': { // old quoting, whilst rich quoting is in labs const html = HtmlUtils.bodyToHtml(payload.event.getContent(), null, {
/// XXX: Not doing rich-text quoting from formatted-body because draft-js returnString: true,
/// has regressed such that when links are quoted, errors are thrown. See emojiOne: false,
/// https://github.com/vector-im/riot-web/issues/4756. });
const body = escape(payload.text); const fragment = this.html.deserialize(html);
if (body) { // FIXME: do we want to put in a permalink to the original quote here?
let content = RichText.htmlToContentState(`<blockquote>${body}</blockquote>`); // If so, what should be the format, and how do we differentiate it from replies?
if (!this.state.isRichTextEnabled) {
content = ContentState.createFromText(RichText.stateToMarkdown(content));
}
const blockMap = content.getBlockMap(); const quote = Block.create('block-quote');
let startSelection = SelectionState.createEmpty(contentState.getFirstBlock().getKey()); if (this.state.isRichTextEnabled) {
contentState = Modifier.splitBlock(contentState, startSelection); let change = editorState.change();
startSelection = SelectionState.createEmpty(contentState.getFirstBlock().getKey()); if (editorState.anchorText.text === '' && editorState.anchorBlock.nodes.size === 1) {
contentState = Modifier.replaceWithFragment(contentState, // replace the current block rather than split the block
startSelection, change = change.replaceNodeByKey(editorState.anchorBlock.key, quote);
blockMap);
startSelection = SelectionState.createEmpty(contentState.getFirstBlock().getKey());
if (this.state.isRichTextEnabled) {
contentState = Modifier.setBlockType(contentState, startSelection, 'blockquote');
} }
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters'); else {
editorState = EditorState.moveSelectionToEnd(editorState); // insert it into the middle of the block (splitting it)
this.onEditorContentChanged(editorState); change = change.insertBlock(quote);
editor.focus(); }
change = change.insertFragmentByKey(quote.key, 0, fragment.document)
.focus();
this.onChange(change);
}
else {
let fragmentChange = fragment.change();
fragmentChange.moveToRangeOf(fragment.document)
.wrapBlock(quote);
// FIXME: handle pills and use commonmark rather than md-serialize
const md = this.md.serialize(fragmentChange.value);
let change = editorState.change()
.insertText(md + '\n\n')
.focus();
this.onChange(change);
} }
} }
break; break;
*/
} }
}; };
@ -555,7 +562,7 @@ export default class MessageComposerInput extends React.Component {
} }
} }
if (this.props.onInputStateChanged) { if (this.props.onInputStateChanged && editorState.blocks.size > 0) {
let blockType = editorState.blocks.first().type; let blockType = editorState.blocks.first().type;
// console.log("onInputStateChanged; current block type is " + blockType + " and marks are " + editorState.activeMarks); // console.log("onInputStateChanged; current block type is " + blockType + " and marks are " + editorState.activeMarks);
@ -740,17 +747,31 @@ export default class MessageComposerInput extends React.Component {
.unwrapBlock('numbered-list'); .unwrapBlock('numbered-list');
return change; return change;
} }
else if (editorState.anchorOffset == 0 && else if (editorState.anchorOffset == 0 && editorState.isCollapsed) {
(this.hasBlock('block-quote') || // turn blocks back into paragraphs
this.hasBlock('heading1') || if ((this.hasBlock('block-quote') ||
this.hasBlock('heading2') || this.hasBlock('heading1') ||
this.hasBlock('heading3') || this.hasBlock('heading2') ||
this.hasBlock('heading4') || this.hasBlock('heading3') ||
this.hasBlock('heading5') || this.hasBlock('heading4') ||
this.hasBlock('heading6') || this.hasBlock('heading5') ||
this.hasBlock('code-block'))) this.hasBlock('heading6') ||
{ this.hasBlock('code-block')))
return change.setBlocks(DEFAULT_NODE); {
return change.setBlocks(DEFAULT_NODE);
}
// remove paragraphs entirely if they're nested
const parent = editorState.document.getParent(editorState.anchorBlock.key);
if (editorState.anchorOffset == 0 &&
this.hasBlock('paragraph') &&
parent.nodes.size == 1 &&
parent.object !== 'document')
{
return change.replaceNodeByKey(editorState.anchorBlock.key, editorState.anchorText)
.collapseToEndOf(parent)
.focus();
}
} }
} }
return; return;
@ -1013,7 +1034,7 @@ export default class MessageComposerInput extends React.Component {
if (!shouldSendHTML) { if (!shouldSendHTML) {
shouldSendHTML = !!editorState.document.findDescendant(node => { shouldSendHTML = !!editorState.document.findDescendant(node => {
// N.B. node.getMarks() might be private? // N.B. node.getMarks() might be private?
return ((node.object === 'block' && node.type !== 'line') || return ((node.object === 'block' && node.type !== 'paragraph') ||
(node.object === 'inline') || (node.object === 'inline') ||
(node.object === 'text' && node.getMarks().size > 0)); (node.object === 'text' && node.getMarks().size > 0));
}); });
@ -1131,13 +1152,13 @@ export default class MessageComposerInput extends React.Component {
// heuristic to handle tall emoji, pills, etc pushing the cursor away from the top // heuristic to handle tall emoji, pills, etc pushing the cursor away from the top
// or bottom of the page. // or bottom of the page.
// XXX: is this going to break on large inline images or top-to-bottom scripts? // XXX: is this going to break on large inline images or top-to-bottom scripts?
const EDGE_THRESHOLD = 8; const EDGE_THRESHOLD = 15;
let navigateHistory = false; let navigateHistory = false;
if (up) { if (up) {
const scrollCorrection = editorNode.scrollTop; const scrollCorrection = editorNode.scrollTop;
const distanceFromTop = cursorRect.top - editorRect.top + scrollCorrection; const distanceFromTop = cursorRect.top - editorRect.top + scrollCorrection;
//console.log(`Cursor distance from editor top is ${distanceFromTop}`); console.log(`Cursor distance from editor top is ${distanceFromTop}`);
if (distanceFromTop < EDGE_THRESHOLD) { if (distanceFromTop < EDGE_THRESHOLD) {
navigateHistory = true; navigateHistory = true;
} }
@ -1146,7 +1167,7 @@ export default class MessageComposerInput extends React.Component {
const scrollCorrection = const scrollCorrection =
editorNode.scrollHeight - editorNode.clientHeight - editorNode.scrollTop; editorNode.scrollHeight - editorNode.clientHeight - editorNode.scrollTop;
const distanceFromBottom = editorRect.bottom - cursorRect.bottom + scrollCorrection; const distanceFromBottom = editorRect.bottom - cursorRect.bottom + scrollCorrection;
//console.log(`Cursor distance from editor bottom is ${distanceFromBottom}`); console.log(`Cursor distance from editor bottom is ${distanceFromBottom}`);
if (distanceFromBottom < EDGE_THRESHOLD) { if (distanceFromBottom < EDGE_THRESHOLD) {
navigateHistory = true; navigateHistory = true;
} }