diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 3d01052ccf..dbc970511a 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -38,14 +38,19 @@ export default class MessageComposer extends React.Component { this.onDownArrow = this.onDownArrow.bind(this); this._tryComplete = this._tryComplete.bind(this); this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this); + this.onToggleFormattingClicked = this.onToggleFormattingClicked.bind(this); + this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this); + this.onInputStateChanged = this.onInputStateChanged.bind(this); this.state = { autocompleteQuery: '', selection: null, - selectionInfo: { + inputState: { style: [], blockType: null, + isRichtextEnabled: true, }, + showFormatting: false, }; } @@ -131,14 +136,17 @@ export default class MessageComposer extends React.Component { }); } - onInputContentChanged(content: string, selection: {start: number, end: number}, selectionInfo) { + onInputContentChanged(content: string, selection: {start: number, end: number}) { this.setState({ autocompleteQuery: content, selection, - selectionInfo, }); } + onInputStateChanged(inputState) { + this.setState({inputState}); + } + onUpArrow() { return this.refs.autocomplete.onUpArrow(); } @@ -161,9 +169,18 @@ export default class MessageComposer extends React.Component { } onFormatButtonClicked(name: "bold" | "italic" | "strike" | "quote" | "bullet" | "numbullet", event) { + event.preventDefault(); this.messageComposerInput.onFormatButtonClicked(name, event); } + onToggleFormattingClicked() { + this.setState({showFormatting: !this.state.showFormatting}); + } + + onToggleMarkdownClicked() { + this.messageComposerInput.enableRichtext(!this.state.inputState.isRichtextEnabled); + } + render() { var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId); var uploadInputStyle = {display: 'none'}; @@ -217,8 +234,11 @@ export default class MessageComposer extends React.Component { ); const formattingButton = ( - ); @@ -232,7 +252,8 @@ export default class MessageComposer extends React.Component { onUpArrow={this.onUpArrow} onDownArrow={this.onDownArrow} tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete - onContentChanged={this.onInputContentChanged} />, + onContentChanged={this.onInputContentChanged} + onInputStateChanged={this.onInputStateChanged} />, formattingButton, uploadButton, hangupButton, @@ -259,7 +280,7 @@ export default class MessageComposer extends React.Component { } - const {style, blockType} = this.state.selectionInfo; + const {style, blockType} = this.state.inputState; const formatButtons = ["bold", "italic", "strike", "quote", "bullet", "numbullet"].map( name => { const active = style.includes(name) || blockType === name; @@ -278,8 +299,17 @@ export default class MessageComposer extends React.Component { {UserSettingsStore.isFeatureEnabled('rich_text_editor') ? -
+
{formatButtons} +
+ +
: null }
diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index aebb1855f3..49b0706499 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -29,10 +29,11 @@ marked.setOptions({ import {Editor, EditorState, RichUtils, CompositeDecorator, convertFromRaw, convertToRaw, Modifier, EditorChangeType, - getDefaultKeyBinding, KeyBindingUtil, ContentState} from 'draft-js'; + getDefaultKeyBinding, KeyBindingUtil, ContentState, SelectionState} from 'draft-js'; import {stateToMarkdown} from 'draft-js-export-markdown'; import classNames from 'classnames'; +import escape from 'lodash/escape'; import MatrixClientPeg from '../../../MatrixClientPeg'; import type {MatrixClient} from 'matrix-js-sdk/lib/matrix'; @@ -42,6 +43,7 @@ import sdk from '../../../index'; import dis from '../../../dispatcher'; import KeyCode from '../../../KeyCode'; +import UserSettingsStore from '../../../UserSettingsStore'; import * as RichText from '../../../RichText'; @@ -90,14 +92,10 @@ export default class MessageComposerInput extends React.Component { this.onTab = this.onTab.bind(this); this.onConfirmAutocompletion = this.onConfirmAutocompletion.bind(this); - let isRichtextEnabled = window.localStorage.getItem('mx_editor_rte_enabled'); - if (isRichtextEnabled == null) { - isRichtextEnabled = 'true'; - } - isRichtextEnabled = isRichtextEnabled === 'true'; + const isRichtextEnabled = UserSettingsStore.isFeatureEnabled('rich_text_editor'); this.state = { - isRichtextEnabled: isRichtextEnabled, + isRichtextEnabled, editorState: null, }; @@ -237,8 +235,18 @@ export default class MessageComposerInput extends React.Component { this.sentHistory.saveLastTextEntry(); } + componentWillUpdate(nextProps, nextState) { + // this is dirty, but moving all this state to MessageComposer is dirtier + if (this.props.onInputStateChanged && nextState !== this.state) { + const state = this.getSelectionInfo(nextState.editorState); + state.isRichtextEnabled = nextState.isRichtextEnabled; + this.props.onInputStateChanged(state); + } + } + onAction(payload) { let editor = this.refs.editor; + let contentState = this.state.editorState.getCurrentContent(); switch (payload.action) { case 'focus_composer': @@ -247,20 +255,44 @@ export default class MessageComposerInput extends React.Component { // TODO change this so we insert a complete user alias - case 'insert_displayname': - if (this.state.editorState.getCurrentContent().hasText()) { - console.log(payload); - let contentState = Modifier.replaceText( - this.state.editorState.getCurrentContent(), - this.state.editorState.getSelection(), - payload.displayname - ); - this.setState({ - editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters'), - }); + case 'insert_displayname': { + contentState = Modifier.replaceText( + contentState, + this.state.editorState.getSelection(), + `${payload.displayname}: ` + ); + let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters'); + editorState = EditorState.forceSelection(editorState, contentState.getSelectionAfter()); + this.setEditorState(editorState); + editor.focus(); + } + break; + + case 'quote': { + let {event: {content: {body, formatted_body}}} = payload.event || {}; + formatted_body = formatted_body || escape(body); + if (formatted_body) { + let content = RichText.HTMLtoContentState(`
${formatted_body}
`); + if (!this.state.isRichtextEnabled) { + content = ContentState.createFromText(stateToMarkdown(content)); + } + + const blockMap = content.getBlockMap(); + let startSelection = SelectionState.createEmpty(contentState.getFirstBlock().getKey()); + contentState = Modifier.splitBlock(contentState, startSelection); + startSelection = SelectionState.createEmpty(contentState.getFirstBlock().getKey()); + contentState = Modifier.replaceWithFragment(contentState, + startSelection, + 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'); + this.setEditorState(editorState); editor.focus(); } - break; + } + break; } } @@ -363,9 +395,8 @@ export default class MessageComposerInput extends React.Component { const textContent = editorState.getCurrentContent().getPlainText(); const selection = RichText.selectionStateToTextOffsets(editorState.getSelection(), editorState.getCurrentContent().getBlocksAsArray()); - const selectionInfo = this.getSelectionInfo(editorState); - this.props.onContentChanged(textContent, selection, selectionInfo); + this.props.onContentChanged(textContent, selection); } } @@ -428,7 +459,8 @@ export default class MessageComposerInput extends React.Component { handleReturn(ev) { if (ev.shiftKey) { - return false; + this.setEditorState(RichUtils.insertSoftNewline(this.state.editorState)); + return true; } const contentState = this.state.editorState.getCurrentContent(); @@ -469,7 +501,7 @@ export default class MessageComposerInput extends React.Component { return true; } - if(this.state.isRichtextEnabled) { + if (this.state.isRichtextEnabled) { contentHTML = RichText.contentStateToHTML(contentState); } else { contentHTML = mdownToHtml(contentText); @@ -618,6 +650,9 @@ export default class MessageComposerInput extends React.Component { return (
+