mirror of https://github.com/vector-im/riot-web
parent
71251293e4
commit
0c0c44b050
|
@ -38,14 +38,19 @@ export default class MessageComposer extends React.Component {
|
||||||
this.onDownArrow = this.onDownArrow.bind(this);
|
this.onDownArrow = this.onDownArrow.bind(this);
|
||||||
this._tryComplete = this._tryComplete.bind(this);
|
this._tryComplete = this._tryComplete.bind(this);
|
||||||
this._onAutocompleteConfirm = this._onAutocompleteConfirm.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 = {
|
this.state = {
|
||||||
autocompleteQuery: '',
|
autocompleteQuery: '',
|
||||||
selection: null,
|
selection: null,
|
||||||
selectionInfo: {
|
inputState: {
|
||||||
style: [],
|
style: [],
|
||||||
blockType: null,
|
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({
|
this.setState({
|
||||||
autocompleteQuery: content,
|
autocompleteQuery: content,
|
||||||
selection,
|
selection,
|
||||||
selectionInfo,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onInputStateChanged(inputState) {
|
||||||
|
this.setState({inputState});
|
||||||
|
}
|
||||||
|
|
||||||
onUpArrow() {
|
onUpArrow() {
|
||||||
return this.refs.autocomplete.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) {
|
onFormatButtonClicked(name: "bold" | "italic" | "strike" | "quote" | "bullet" | "numbullet", event) {
|
||||||
|
event.preventDefault();
|
||||||
this.messageComposerInput.onFormatButtonClicked(name, event);
|
this.messageComposerInput.onFormatButtonClicked(name, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onToggleFormattingClicked() {
|
||||||
|
this.setState({showFormatting: !this.state.showFormatting});
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggleMarkdownClicked() {
|
||||||
|
this.messageComposerInput.enableRichtext(!this.state.inputState.isRichtextEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||||
var uploadInputStyle = {display: 'none'};
|
var uploadInputStyle = {display: 'none'};
|
||||||
|
@ -217,8 +234,11 @@ export default class MessageComposer extends React.Component {
|
||||||
);
|
);
|
||||||
|
|
||||||
const formattingButton = (
|
const formattingButton = (
|
||||||
<img title="Text Formatting"
|
<img className="mx_MessageComposer_formatting"
|
||||||
|
title="Show Text Formatting Toolbar"
|
||||||
src="img/button-text-formatting.svg"
|
src="img/button-text-formatting.svg"
|
||||||
|
onClick={this.onToggleFormattingClicked}
|
||||||
|
style={{visibility: this.state.showFormatting ? 'hidden' : 'visible'}}
|
||||||
key="controls_formatting" />
|
key="controls_formatting" />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -232,7 +252,8 @@ export default class MessageComposer extends React.Component {
|
||||||
onUpArrow={this.onUpArrow}
|
onUpArrow={this.onUpArrow}
|
||||||
onDownArrow={this.onDownArrow}
|
onDownArrow={this.onDownArrow}
|
||||||
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
|
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
|
||||||
onContentChanged={this.onInputContentChanged} />,
|
onContentChanged={this.onInputContentChanged}
|
||||||
|
onInputStateChanged={this.onInputStateChanged} />,
|
||||||
formattingButton,
|
formattingButton,
|
||||||
uploadButton,
|
uploadButton,
|
||||||
hangupButton,
|
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(
|
const formatButtons = ["bold", "italic", "strike", "quote", "bullet", "numbullet"].map(
|
||||||
name => {
|
name => {
|
||||||
const active = style.includes(name) || blockType === name;
|
const active = style.includes(name) || blockType === name;
|
||||||
|
@ -278,8 +299,17 @@ export default class MessageComposer extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{UserSettingsStore.isFeatureEnabled('rich_text_editor') ?
|
{UserSettingsStore.isFeatureEnabled('rich_text_editor') ?
|
||||||
<div className="mx_MessageComposer_formatbar">
|
<div className="mx_MessageComposer_formatbar" style={this.state.showFormatting ? {} : {display: 'none'}}>
|
||||||
{formatButtons}
|
{formatButtons}
|
||||||
|
<div style={{flex: 1}}></div>
|
||||||
|
<img title={`Turn Markdown ${this.state.inputState.isRichtextEnabled ? 'on' : 'off'}`}
|
||||||
|
onClick={this.onToggleMarkdownClicked}
|
||||||
|
className="mx_MessageComposer_formatbar_markdown"
|
||||||
|
src={`img/button-md-${!this.state.inputState.isRichtextEnabled}.png`} />
|
||||||
|
<img title="Hide Text Formatting Toolbar"
|
||||||
|
onClick={this.onToggleFormattingClicked}
|
||||||
|
className="mx_MessageComposer_formatbar_cancel"
|
||||||
|
src="img/icon-text-cancel.svg" />
|
||||||
</div> : null
|
</div> : null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -29,10 +29,11 @@ marked.setOptions({
|
||||||
|
|
||||||
import {Editor, EditorState, RichUtils, CompositeDecorator,
|
import {Editor, EditorState, RichUtils, CompositeDecorator,
|
||||||
convertFromRaw, convertToRaw, Modifier, EditorChangeType,
|
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 {stateToMarkdown} from 'draft-js-export-markdown';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import escape from 'lodash/escape';
|
||||||
|
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
|
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
|
||||||
|
@ -42,6 +43,7 @@ import sdk from '../../../index';
|
||||||
|
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import KeyCode from '../../../KeyCode';
|
import KeyCode from '../../../KeyCode';
|
||||||
|
import UserSettingsStore from '../../../UserSettingsStore';
|
||||||
|
|
||||||
import * as RichText from '../../../RichText';
|
import * as RichText from '../../../RichText';
|
||||||
|
|
||||||
|
@ -90,14 +92,10 @@ export default class MessageComposerInput extends React.Component {
|
||||||
this.onTab = this.onTab.bind(this);
|
this.onTab = this.onTab.bind(this);
|
||||||
this.onConfirmAutocompletion = this.onConfirmAutocompletion.bind(this);
|
this.onConfirmAutocompletion = this.onConfirmAutocompletion.bind(this);
|
||||||
|
|
||||||
let isRichtextEnabled = window.localStorage.getItem('mx_editor_rte_enabled');
|
const isRichtextEnabled = UserSettingsStore.isFeatureEnabled('rich_text_editor');
|
||||||
if (isRichtextEnabled == null) {
|
|
||||||
isRichtextEnabled = 'true';
|
|
||||||
}
|
|
||||||
isRichtextEnabled = isRichtextEnabled === 'true';
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isRichtextEnabled: isRichtextEnabled,
|
isRichtextEnabled,
|
||||||
editorState: null,
|
editorState: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -237,8 +235,18 @@ export default class MessageComposerInput extends React.Component {
|
||||||
this.sentHistory.saveLastTextEntry();
|
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) {
|
onAction(payload) {
|
||||||
let editor = this.refs.editor;
|
let editor = this.refs.editor;
|
||||||
|
let contentState = this.state.editorState.getCurrentContent();
|
||||||
|
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'focus_composer':
|
case 'focus_composer':
|
||||||
|
@ -247,20 +255,44 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
// TODO change this so we insert a complete user alias
|
// TODO change this so we insert a complete user alias
|
||||||
|
|
||||||
case 'insert_displayname':
|
case 'insert_displayname': {
|
||||||
if (this.state.editorState.getCurrentContent().hasText()) {
|
contentState = Modifier.replaceText(
|
||||||
console.log(payload);
|
contentState,
|
||||||
let contentState = Modifier.replaceText(
|
this.state.editorState.getSelection(),
|
||||||
this.state.editorState.getCurrentContent(),
|
`${payload.displayname}: `
|
||||||
this.state.editorState.getSelection(),
|
);
|
||||||
payload.displayname
|
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
|
||||||
);
|
editorState = EditorState.forceSelection(editorState, contentState.getSelectionAfter());
|
||||||
this.setState({
|
this.setEditorState(editorState);
|
||||||
editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters'),
|
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(`<blockquote>${formatted_body}</blockquote>`);
|
||||||
|
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();
|
editor.focus();
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,9 +395,8 @@ export default class MessageComposerInput extends React.Component {
|
||||||
const textContent = editorState.getCurrentContent().getPlainText();
|
const textContent = editorState.getCurrentContent().getPlainText();
|
||||||
const selection = RichText.selectionStateToTextOffsets(editorState.getSelection(),
|
const selection = RichText.selectionStateToTextOffsets(editorState.getSelection(),
|
||||||
editorState.getCurrentContent().getBlocksAsArray());
|
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) {
|
handleReturn(ev) {
|
||||||
if (ev.shiftKey) {
|
if (ev.shiftKey) {
|
||||||
return false;
|
this.setEditorState(RichUtils.insertSoftNewline(this.state.editorState));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentState = this.state.editorState.getCurrentContent();
|
const contentState = this.state.editorState.getCurrentContent();
|
||||||
|
@ -469,7 +501,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.state.isRichtextEnabled) {
|
if (this.state.isRichtextEnabled) {
|
||||||
contentHTML = RichText.contentStateToHTML(contentState);
|
contentHTML = RichText.contentStateToHTML(contentState);
|
||||||
} else {
|
} else {
|
||||||
contentHTML = mdownToHtml(contentText);
|
contentHTML = mdownToHtml(contentText);
|
||||||
|
@ -618,6 +650,9 @@ export default class MessageComposerInput extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div className={className}
|
<div className={className}
|
||||||
onClick={ this.onInputClick }>
|
onClick={ this.onInputClick }>
|
||||||
|
<img className="mx_MessageComposer_input_markdownIndicator"
|
||||||
|
title={`Markdown is ${this.state.isRichtextEnabled ? 'disabled' : 'enabled'}`}
|
||||||
|
src={`img/button-md-${!this.state.isRichtextEnabled}.png`} />
|
||||||
<Editor ref="editor"
|
<Editor ref="editor"
|
||||||
placeholder="Type a message…"
|
placeholder="Type a message…"
|
||||||
editorState={this.state.editorState}
|
editorState={this.state.editorState}
|
||||||
|
@ -654,4 +689,6 @@ MessageComposerInput.propTypes = {
|
||||||
|
|
||||||
// attempts to confirm currently selected completion, returns whether actually confirmed
|
// attempts to confirm currently selected completion, returns whether actually confirmed
|
||||||
tryComplete: React.PropTypes.func,
|
tryComplete: React.PropTypes.func,
|
||||||
|
|
||||||
|
onInputStateChanged: React.PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue