apply review feedback from @lukebarnard1

(cherry picked from commit 37d4bce)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Matthew Hodgson 2018-07-09 00:52:27 +01:00 committed by Michael Telatynski
parent 83f2614919
commit 021409aafe
No known key found for this signature in database
GPG key ID: 3F879DA5AD802A5E
6 changed files with 38 additions and 116 deletions

View file

@ -112,42 +112,6 @@ export function charactersToImageNode(alt, useSvg, ...unicode) {
/>; />;
} }
/*
export function processHtmlForSending(html: string): string {
const contentDiv = document.createElement('div');
contentDiv.innerHTML = html;
if (contentDiv.children.length === 0) {
return contentDiv.innerHTML;
}
let contentHTML = "";
for (let i=0; i < contentDiv.children.length; i++) {
const element = contentDiv.children[i];
if (element.tagName.toLowerCase() === 'p') {
contentHTML += element.innerHTML;
// Don't add a <br /> for the last <p>
if (i !== contentDiv.children.length - 1) {
contentHTML += '<br />';
}
} else if (element.tagName.toLowerCase() === 'pre') {
// Replace "<br>\n" with "\n" within `<pre>` tags because the <br> is
// redundant. This is a workaround for a bug in draft-js-export-html:
// https://github.com/sstur/draft-js-export-html/issues/62
contentHTML += '<pre>' +
element.innerHTML.replace(/<br>\n/g, '\n').trim() +
'</pre>';
} else {
const temp = document.createElement('div');
temp.appendChild(element.cloneNode(true));
contentHTML += temp.innerHTML;
}
}
return contentHTML;
}
*/
/* /*
* Given an untrusted HTML string, return a React node with an sanitized version * Given an untrusted HTML string, return a React node with an sanitized version
* of that HTML. * of that HTML.

View file

@ -180,14 +180,6 @@ export default class Markdown {
if (is_multi_line(node) && node.next) this.lit('\n\n'); if (is_multi_line(node) && node.next) this.lit('\n\n');
}; };
// convert MD links into console-friendly ' < http://foo >' style links
// ...except given this function never gets called with links, it's useless.
// renderer.link = function(node, entering) {
// if (!entering) {
// this.lit(` < ${node.destination} >`);
// }
// };
return renderer.render(this.parsed); return renderer.render(this.parsed);
} }
} }

View file

@ -29,9 +29,9 @@ import NotifProvider from './NotifProvider';
import Promise from 'bluebird'; import Promise from 'bluebird';
export type SelectionRange = { export type SelectionRange = {
beginning: boolean, beginning: boolean, // whether the selection is in the first block of the editor or not
start: number, start: number, // byte offset relative to the start anchor of the current editor selection.
end: number end: number, // byte offset relative to the end anchor of the current editor selection.
}; };
export type Completion = { export type Completion = {

View file

@ -111,7 +111,7 @@ export default class UserProvider extends AutocompleteProvider {
// relies on the length of the entity === length of the text in the decoration. // relies on the length of the entity === length of the text in the decoration.
completion: user.rawDisplayName.replace(' (IRC)', ''), completion: user.rawDisplayName.replace(' (IRC)', ''),
completionId: user.userId, completionId: user.userId,
suffix: (selection.beginning && range.start === 0) ? ': ' : ' ', suffix: (selection.beginning && selection.start === 0) ? ': ' : ' ',
href: makeUserPermalink(user.userId), href: makeUserPermalink(user.userId),
component: ( component: (
<PillCompletion <PillCompletion

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, Block, Inline, Text, Range, Node } from 'slate'; import { Value, Document, Block, Inline, Text, Range, Node } from 'slate';
import type { Change } from 'slate'; import type { Change } from 'slate';
import Html from 'slate-html-serializer'; import Html from 'slate-html-serializer';
@ -29,10 +29,6 @@ import Md from 'slate-md-serializer';
import Plain from 'slate-plain-serializer'; import Plain from 'slate-plain-serializer';
import PlainWithPillsSerializer from "../../../autocomplete/PlainWithPillsSerializer"; import PlainWithPillsSerializer from "../../../autocomplete/PlainWithPillsSerializer";
// import {Editor, EditorState, RichUtils, CompositeDecorator, Modifier,
// getDefaultKeyBinding, KeyBindingUtil, ContentState, ContentBlock, SelectionState,
// Entity} from 'draft-js';
import classNames from 'classnames'; import classNames from 'classnames';
import Promise from 'bluebird'; import Promise from 'bluebird';
@ -170,6 +166,16 @@ export default class MessageComposerInput extends React.Component {
this.client = MatrixClientPeg.get(); this.client = MatrixClientPeg.get();
// track whether we should be trying to show autocomplete suggestions on the current editor
// contents. currently it's only suppressed when navigating history to avoid ugly flashes
// of unexpected corrections as you navigate.
// XXX: should this be in state?
this.suppressAutoComplete = false;
// track whether we've just pressed an arrowkey left or right in order to skip void nodes.
// see https://github.com/ianstormtaylor/slate/issues/762#issuecomment-304855095
this.direction = '';
this.plainWithMdPills = new PlainWithPillsSerializer({ pillFormat: 'md' }); this.plainWithMdPills = new PlainWithPillsSerializer({ pillFormat: 'md' });
this.plainWithIdPills = new PlainWithPillsSerializer({ pillFormat: 'id' }); this.plainWithIdPills = new PlainWithPillsSerializer({ pillFormat: 'id' });
this.plainWithPlainPills = new PlainWithPillsSerializer({ pillFormat: 'plain' }); this.plainWithPlainPills = new PlainWithPillsSerializer({ pillFormat: 'plain' });
@ -177,6 +183,8 @@ export default class MessageComposerInput extends React.Component {
this.md = new Md({ this.md = new Md({
rules: [ rules: [
{ {
// if serialize returns undefined it falls through to the default hardcoded
// serialization rules
serialize: (obj, children) => { serialize: (obj, children) => {
if (obj.object !== 'inline') return; if (obj.object !== 'inline') return;
switch (obj.type) { switch (obj.type) {
@ -288,9 +296,6 @@ export default class MessageComposerInput extends React.Component {
} }
] ]
}); });
this.suppressAutoComplete = false;
this.direction = '';
} }
/* /*
@ -298,7 +303,8 @@ export default class MessageComposerInput extends React.Component {
* - whether we've got rich text mode enabled * - whether we've got rich text mode enabled
* - contentState was passed in * - contentState was passed in
*/ */
createEditorState(richText: boolean, editorState: ?Value): Value { createEditorState(richText: boolean, // eslint-disable-line no-unused-vars
editorState: ?Value): Value {
if (editorState instanceof Value) { if (editorState instanceof Value) {
return editorState; return editorState;
} }
@ -371,7 +377,6 @@ export default class MessageComposerInput extends React.Component {
let fragmentChange = fragment.change(); let fragmentChange = fragment.change();
fragmentChange.moveToRangeOf(fragment.document) fragmentChange.moveToRangeOf(fragment.document)
.wrapBlock(quote); .wrapBlock(quote);
//.setBlocks('block-quote');
// FIXME: handle pills and use commonmark rather than md-serialize // FIXME: handle pills and use commonmark rather than md-serialize
const md = this.md.serialize(fragmentChange.value); const md = this.md.serialize(fragmentChange.value);
@ -459,6 +464,7 @@ export default class MessageComposerInput extends React.Component {
if (this.direction !== '') { if (this.direction !== '') {
const focusedNode = editorState.focusInline || editorState.focusText; const focusedNode = editorState.focusInline || editorState.focusText;
if (focusedNode.isVoid) { if (focusedNode.isVoid) {
// XXX: does this work in RTL?
const edge = this.direction === 'Previous' ? 'End' : 'Start'; const edge = this.direction === 'Previous' ? 'End' : 'Start';
if (editorState.isCollapsed) { if (editorState.isCollapsed) {
change = change[`collapseTo${ edge }Of${ this.direction }Text`](); change = change[`collapseTo${ edge }Of${ this.direction }Text`]();
@ -478,13 +484,6 @@ export default class MessageComposerInput extends React.Component {
this.onFinishedTyping(); this.onFinishedTyping();
} }
/*
// XXX: what was this ever doing?
if (!state.hasOwnProperty('originalEditorState')) {
state.originalEditorState = null;
}
*/
if (editorState.startText !== null) { if (editorState.startText !== null) {
const text = editorState.startText.text; const text = editorState.startText.text;
const currentStartOffset = editorState.startOffset; const currentStartOffset = editorState.startOffset;
@ -512,9 +511,7 @@ export default class MessageComposerInput extends React.Component {
} }
// emojioneify any emoji // emojioneify any emoji
editorState.document.getTexts().forEach(node => {
// XXX: is getTextsAsArray a private API?
editorState.document.getTextsAsArray().forEach(node => {
if (node.text !== '' && HtmlUtils.containsEmoji(node.text)) { if (node.text !== '' && HtmlUtils.containsEmoji(node.text)) {
let match; let match;
while ((match = EMOJI_REGEX.exec(node.text)) !== null) { while ((match = EMOJI_REGEX.exec(node.text)) !== null) {
@ -546,36 +543,6 @@ export default class MessageComposerInput extends React.Component {
editorState = change.value; editorState = change.value;
} }
/*
const currentBlock = editorState.getSelection().getStartKey();
const currentSelection = editorState.getSelection();
const currentStartOffset = editorState.getSelection().getStartOffset();
const block = editorState.getCurrentContent().getBlockForKey(currentBlock);
const text = block.getText();
const entityBeforeCurrentOffset = block.getEntityAt(currentStartOffset - 1);
const entityAtCurrentOffset = block.getEntityAt(currentStartOffset);
// If the cursor is on the boundary between an entity and a non-entity and the
// text before the cursor has whitespace at the end, set the entity state of the
// character before the cursor (the whitespace) to null. This allows the user to
// stop editing the link.
if (entityBeforeCurrentOffset && !entityAtCurrentOffset &&
/\s$/.test(text.slice(0, currentStartOffset))) {
editorState = RichUtils.toggleLink(
editorState,
currentSelection.merge({
anchorOffset: currentStartOffset - 1,
focusOffset: currentStartOffset,
}),
null,
);
// Reset selection
editorState = EditorState.forceSelection(editorState, currentSelection);
}
*/
if (this.props.onInputStateChanged && editorState.blocks.size > 0) { 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);
@ -605,7 +572,6 @@ export default class MessageComposerInput extends React.Component {
editor_state: editorState, editor_state: editorState,
}); });
/* Since a modification was made, set originalEditorState to null, since newState is now our original */
this.setState({ this.setState({
editorState, editorState,
originalEditorState: originalEditorState || null originalEditorState: originalEditorState || null
@ -683,7 +649,7 @@ export default class MessageComposerInput extends React.Component {
hasMark = type => { hasMark = type => {
const { editorState } = this.state const { editorState } = this.state
return editorState.activeMarks.some(mark => mark.type == type) return editorState.activeMarks.some(mark => mark.type === type)
}; };
/** /**
@ -695,7 +661,7 @@ export default class MessageComposerInput extends React.Component {
hasBlock = type => { hasBlock = type => {
const { editorState } = this.state const { editorState } = this.state
return editorState.blocks.some(node => node.type == type) return editorState.blocks.some(node => node.type === type)
}; };
onKeyDown = (ev: KeyboardEvent, change: Change, editor: Editor) => { onKeyDown = (ev: KeyboardEvent, change: Change, editor: Editor) => {
@ -828,7 +794,7 @@ export default class MessageComposerInput extends React.Component {
// Handle the extra wrapping required for list buttons. // Handle the extra wrapping required for list buttons.
const isList = this.hasBlock('list-item'); const isList = this.hasBlock('list-item');
const isType = editorState.blocks.some(block => { const isType = editorState.blocks.some(block => {
return !!document.getClosest(block.key, parent => parent.type == type); return !!document.getClosest(block.key, parent => parent.type === type);
}); });
if (isList && isType) { if (isList && isType) {
@ -839,7 +805,7 @@ export default class MessageComposerInput extends React.Component {
} else if (isList) { } else if (isList) {
change change
.unwrapBlock( .unwrapBlock(
type == 'bulleted-list' ? 'numbered-list' : 'bulleted-list' type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list'
) )
.wrapBlock(type); .wrapBlock(type);
} else { } else {
@ -1009,7 +975,7 @@ export default class MessageComposerInput extends React.Component {
let contentHTML; let contentHTML;
// only look for commands if the first block contains simple unformatted text // only look for commands if the first block contains simple unformatted text
// i.e. no pills or rich-text formatting. // i.e. no pills or rich-text formatting and begins with a /.
let cmd, commandText; let cmd, commandText;
const firstChild = editorState.document.nodes.get(0); const firstChild = editorState.document.nodes.get(0);
const firstGrandChild = firstChild && firstChild.nodes.get(0); const firstGrandChild = firstChild && firstChild.nodes.get(0);
@ -1090,8 +1056,8 @@ export default class MessageComposerInput extends React.Component {
// didn't contain any formatting in the first place... // didn't contain any formatting in the first place...
contentText = mdWithPills.toPlaintext(); contentText = mdWithPills.toPlaintext();
} else { } else {
// to avoid ugliness clients which can't parse HTML we don't send pills // to avoid ugliness on clients which ignore the HTML body we don't
// in the plaintext body. // send pills in the plaintext body.
contentText = this.plainWithPlainPills.serialize(editorState); contentText = this.plainWithPlainPills.serialize(editorState);
contentHTML = mdWithPills.toHTML(); contentHTML = mdWithPills.toHTML();
} }
@ -1255,11 +1221,8 @@ export default class MessageComposerInput extends React.Component {
// Move selection to the end of the selected history // Move selection to the end of the selected history
const change = editorState.change().collapseToEndOf(editorState.document); const change = editorState.change().collapseToEndOf(editorState.document);
// XXX: should we be calling this.onChange(change) now? // We don't call this.onChange(change) now, as fixups on stuff like emoji
// Answer: yes, if we want it to do any of the fixups on stuff like emoji. // should already have been done and persisted in the history.
// however, this should already have been done and persisted in the history,
// so shouldn't be necessary.
editorState = change.value; editorState = change.value;
this.suppressAutoComplete = true; this.suppressAutoComplete = true;
@ -1362,6 +1325,8 @@ export default class MessageComposerInput extends React.Component {
.insertText(suffix) .insertText(suffix)
.focus(); .focus();
} }
// for good hygiene, keep editorState updated to track the result of the change
// even though we don't do anything subsequently with it
editorState = change.value; editorState = change.value;
this.onChange(change, activeEditorState); this.onChange(change, activeEditorState);
@ -1460,10 +1425,11 @@ export default class MessageComposerInput extends React.Component {
}; };
onFormatButtonClicked = (name, e) => { onFormatButtonClicked = (name, e) => {
e.preventDefault(); // don't steal focus from the editor! e.preventDefault();
// XXX: horrible evil hack to ensure the editor is focused so the act // XXX: horrible evil hack to ensure the editor is focused so the act
// of focusing it doesn't then cancel the format button being pressed // of focusing it doesn't then cancel the format button being pressed
// FIXME: can we just tell handleKeyCommand's change to invoke .focus()?
if (document.activeElement && document.activeElement.className !== 'mx_MessageComposer_editor') { if (document.activeElement && document.activeElement.className !== 'mx_MessageComposer_editor') {
this.refs.editor.focus(); this.refs.editor.focus();
setTimeout(()=>{ setTimeout(()=>{

View file

@ -10,7 +10,6 @@ const MessageComposerInput = sdk.getComponent('views.rooms.MessageComposerInput'
import MatrixClientPeg from '../../../../src/MatrixClientPeg'; import MatrixClientPeg from '../../../../src/MatrixClientPeg';
import RoomMember from 'matrix-js-sdk'; import RoomMember from 'matrix-js-sdk';
/*
function addTextToDraft(text) { function addTextToDraft(text) {
const components = document.getElementsByClassName('public-DraftEditor-content'); const components = document.getElementsByClassName('public-DraftEditor-content');
if (components && components.length) { if (components && components.length) {
@ -21,7 +20,9 @@ function addTextToDraft(text) {
} }
} }
describe('MessageComposerInput', () => { // FIXME: These tests need to be updated from Draft to Slate.
xdescribe('MessageComposerInput', () => {
let parentDiv = null, let parentDiv = null,
sandbox = null, sandbox = null,
client = null, client = null,
@ -300,5 +301,4 @@ describe('MessageComposerInput', () => {
expect(spy.args[0][1].body).toEqual('[Click here](https://some.lovely.url)'); expect(spy.args[0][1].body).toEqual('[Click here](https://some.lovely.url)');
expect(spy.args[0][1].formatted_body).toEqual('<a href="https://some.lovely.url">Click here</a>'); expect(spy.args[0][1].formatted_body).toEqual('<a href="https://some.lovely.url">Click here</a>');
}); });
}); });
*/