diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 14f52832f6..72d31cfddd 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -91,6 +91,15 @@ limitations under the License. overflow: auto; } +// FIXME: rather unpleasant hack to get rid of
margins. +// really we should be mixing in markdown-body from gfm.css instead +.mx_MessageComposer_editor > :first-child { + margin-top: 0 ! important; +} +.mx_MessageComposer_editor > :last-child { + margin-bottom: 0 ! important; +} + @keyframes visualbell { from { background-color: #faa } diff --git a/src/RichText.js b/src/RichText.js index 50ed33d803..e3162a4e2c 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -61,14 +61,6 @@ export function stateToMarkdown(state) { ''); // this is *not* a zero width space, trust me :) } -export const editorStateToHTML = (editorState: Value) => { - return Html.deserialize(editorState); -} - -export function htmlToEditorState(html: string): Value { - return Html.serialize(html); -} - export function unicodeToEmojiUri(str) { let replaceWith, unicode, alt; if ((!emojione.unicodeAlt) || (emojione.sprites)) { diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index acd7c0fab3..ae4d5b6264 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -145,7 +145,26 @@ export default class MessageComposerInput extends React.Component { this.plainWithIdPills = new PlainWithPillsSerializer({ pillFormat: 'id' }); this.plainWithPlainPills = new PlainWithPillsSerializer({ pillFormat: 'plain' }); this.md = new Md(); - //this.html = new Html(); // not used atm + this.html = new Html({ + rules: [ + { + serialize: (obj, children) => { + if (obj.object === 'block' || obj.object === 'inline') { + return this.renderNode({ + node: obj, + children: children, + }); + } + else if (obj.object === 'mark') { + return this.renderMark({ + mark: obj, + children: children, + }); + } + } + } + ] + }); this.suppressAutoComplete = false; this.direction = ''; @@ -397,27 +416,29 @@ export default class MessageComposerInput extends React.Component { editorState = EditorState.forceSelection(editorState, currentSelection); } */ - const text = editorState.startText.text; - const currentStartOffset = editorState.startOffset; + if (editorState.startText !== null) { + const text = editorState.startText.text; + const currentStartOffset = editorState.startOffset; - // Automatic replacement of plaintext emoji to Unicode emoji - if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) { - // The first matched group includes just the matched plaintext emoji - const emojiMatch = REGEX_EMOJI_WHITESPACE.exec(text.slice(0, currentStartOffset)); - if (emojiMatch) { - // plaintext -> hex unicode - const emojiUc = asciiList[emojiMatch[1]]; - // hex unicode -> shortname -> actual unicode - const unicodeEmoji = shortnameToUnicode(EMOJI_UNICODE_TO_SHORTNAME[emojiUc]); + // Automatic replacement of plaintext emoji to Unicode emoji + if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) { + // The first matched group includes just the matched plaintext emoji + const emojiMatch = REGEX_EMOJI_WHITESPACE.exec(text.slice(0, currentStartOffset)); + if (emojiMatch) { + // plaintext -> hex unicode + const emojiUc = asciiList[emojiMatch[1]]; + // hex unicode -> shortname -> actual unicode + const unicodeEmoji = shortnameToUnicode(EMOJI_UNICODE_TO_SHORTNAME[emojiUc]); - const range = Range.create({ - anchorKey: editorState.selection.startKey, - anchorOffset: currentStartOffset - emojiMatch[1].length - 1, - focusKey: editorState.selection.startKey, - focusOffset: currentStartOffset, - }); - change = change.insertTextAtRange(range, unicodeEmoji); - editorState = change.value; + const range = Range.create({ + anchorKey: editorState.selection.startKey, + anchorOffset: currentStartOffset - emojiMatch[1].length - 1, + focusKey: editorState.selection.startKey, + focusOffset: currentStartOffset, + }); + change = change.insertTextAtRange(range, unicodeEmoji); + editorState = change.value; + } } } @@ -444,13 +465,15 @@ export default class MessageComposerInput extends React.Component { let editorState = null; if (enabled) { + // for simplicity when roundtripping, we use slate-md-serializer rather than commonmark + editorState = this.md.deserialize(this.plainWithMdPills.serialize(this.state.editorState)); + + // the alternative would be something like: + // // const sourceWithPills = this.plainWithMdPills.serialize(this.state.editorState); // const markdown = new Markdown(sourceWithPills); // editorState = this.html.deserialize(markdown.toHTML()); - // we don't really want a custom MD parser hanging around, but the - // alternative would be: - editorState = this.md.deserialize(this.plainWithMdPills.serialize(this.state.editorState)); } else { // let markdown = RichText.stateToMarkdown(this.state.editorState.getCurrentContent()); // value = ContentState.createFromText(markdown); @@ -547,6 +570,8 @@ export default class MessageComposerInput extends React.Component { let newState: ?Value = null; + const DEFAULT_NODE = 'paragraph'; + // Draft handles rich text mode commands by default but we need to do it ourselves for Markdown. if (this.state.isRichtextEnabled) { const type = command; @@ -725,11 +750,14 @@ export default class MessageComposerInput extends React.Component { */ handleReturn = (ev) => { if (ev.shiftKey) { + // FIXME: we should insert a will be split into two paragraphs
+ // and it'll look like a double line-break.
return;
}
if (this.state.editorState.blocks.some(
- block => block in ['code-block', 'block-quote', 'bulleted-list', 'numbered-list']
+ block => ['code-block', 'block-quote', 'list-item'].includes(block.type)
)) {
// allow the user to terminate blocks by hitting return rather than sending a msg
return;
@@ -788,47 +816,25 @@ export default class MessageComposerInput extends React.Component {
const mustSendHTML = Boolean(replyingToEv);
if (this.state.isRichtextEnabled) {
-/*
// We should only send HTML if any block is styled or contains inline style
let shouldSendHTML = false;
if (mustSendHTML) shouldSendHTML = true;
- const blocks = contentState.getBlocksAsArray();
- if (blocks.some((block) => block.getType() !== 'unstyled')) {
- shouldSendHTML = true;
- } else {
- const characterLists = blocks.map((block) => block.getCharacterList());
- // For each block of characters, determine if any inline styles are applied
- // and if yes, send HTML
- characterLists.forEach((characters) => {
- const numberOfStylesForCharacters = characters.map(
- (character) => character.getStyle().toArray().length,
- ).toArray();
- // If any character has more than 0 inline styles applied, send HTML
- if (numberOfStylesForCharacters.some((styles) => styles > 0)) {
- shouldSendHTML = true;
- }
- });
- }
if (!shouldSendHTML) {
- const hasLink = blocks.some((block) => {
- return block.getCharacterList().filter((c) => {
- const entityKey = c.getEntity();
- return entityKey && contentState.getEntity(entityKey).getType() === 'LINK';
- }).size > 0;
+ shouldSendHTML = !!editorState.document.findDescendant(node => {
+ // N.B. node.getMarks() might be private?
+ return ((node.object === 'block' && node.type !== 'line') ||
+ (node.object === 'inline') ||
+ (node.object === 'text' && node.getMarks().size > 0));
});
- shouldSendHTML = hasLink;
}
-*/
+
contentText = this.plainWithPlainPills.serialize(editorState);
if (contentText === '') return true;
- let shouldSendHTML = true;
if (shouldSendHTML) {
- contentHTML = HtmlUtils.processHtmlForSending(
- RichText.editorStateToHTML(editorState),
- );
+ contentHTML = this.html.serialize(editorState); // HtmlUtils.processHtmlForSending();
}
} else {
const sourceWithPills = this.plainWithMdPills.serialize(editorState);
@@ -1047,7 +1053,7 @@ export default class MessageComposerInput extends React.Component {
marks: editorState.activeMarks,
// XXX: shouldn't we return all the types of blocks in the current selection,
// not just the anchor?
- blockType: editorState.anchorBlock.type,
+ blockType: editorState.anchorBlock ? editorState.anchorBlock.type : null,
};
}
@@ -1121,6 +1127,10 @@ export default class MessageComposerInput extends React.Component {
const { attributes, children, node, isSelected } = props;
switch (node.type) {
+ case 'line':
+ // ideally we'd return { children }
, but as this isn't
+ // a valid react component, we don't have much choice.
+ return
{children}
; case 'block-quote': @@ -1138,7 +1148,7 @@ export default class MessageComposerInput extends React.Component { case 'numbered-list': return{children}
{children}
;
case 'pill': {
const { data } = node;
const url = data.get('url');
@@ -1187,15 +1197,15 @@ export default class MessageComposerInput extends React.Component {
const { children, mark, attributes } = props;
switch (mark.type) {
case 'bold':
- return {children};
+ return {children};
case 'italic':
- return {children};
+ return {children};
case 'code':
- return {children}
;
+ return {children}
;
case 'underline':
- return {children};
+ return {children};
case 'strikethrough':
- return