WIP (doesn't build yet) replacing draft with slate
This commit is contained in:
parent
44d92bb32e
commit
75a2be1a8d
4 changed files with 125 additions and 88 deletions
|
@ -58,9 +58,6 @@
|
||||||
"classnames": "^2.1.2",
|
"classnames": "^2.1.2",
|
||||||
"commonmark": "^0.28.1",
|
"commonmark": "^0.28.1",
|
||||||
"counterpart": "^0.18.0",
|
"counterpart": "^0.18.0",
|
||||||
"draft-js": "^0.11.0-alpha",
|
|
||||||
"draft-js-export-html": "^0.6.0",
|
|
||||||
"draft-js-export-markdown": "^0.3.0",
|
|
||||||
"emojione": "2.2.7",
|
"emojione": "2.2.7",
|
||||||
"file-saver": "^1.3.3",
|
"file-saver": "^1.3.3",
|
||||||
"filesize": "3.5.6",
|
"filesize": "3.5.6",
|
||||||
|
@ -84,6 +81,10 @@
|
||||||
"react-beautiful-dnd": "^4.0.1",
|
"react-beautiful-dnd": "^4.0.1",
|
||||||
"react-dom": "^15.6.0",
|
"react-dom": "^15.6.0",
|
||||||
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
|
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
|
||||||
|
"slate": "^0.33.4",
|
||||||
|
"slate-react": "^0.12.4",
|
||||||
|
"slate-html-serializer": "^0.6.1",
|
||||||
|
"slate-md-serializer": "^3.0.3",
|
||||||
"sanitize-html": "^1.14.1",
|
"sanitize-html": "^1.14.1",
|
||||||
"text-encoding-utf-8": "^1.0.1",
|
"text-encoding-utf-8": "^1.0.1",
|
||||||
"url": "^0.11.0",
|
"url": "^0.11.0",
|
||||||
|
|
|
@ -15,38 +15,55 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ContentState, convertToRaw, convertFromRaw} from 'draft-js';
|
import { Value } from 'slate';
|
||||||
|
import Html from 'slate-html-serializer';
|
||||||
|
import Markdown as Md from 'slate-md-serializer';
|
||||||
|
import Plain from 'slate-plain-serializer';
|
||||||
import * as RichText from './RichText';
|
import * as RichText from './RichText';
|
||||||
import Markdown from './Markdown';
|
import Markdown from './Markdown';
|
||||||
|
|
||||||
import _clamp from 'lodash/clamp';
|
import _clamp from 'lodash/clamp';
|
||||||
|
|
||||||
type MessageFormat = 'html' | 'markdown';
|
type MessageFormat = 'rich' | 'markdown';
|
||||||
|
|
||||||
class HistoryItem {
|
class HistoryItem {
|
||||||
|
|
||||||
// Keeping message for backwards-compatibility
|
// Keeping message for backwards-compatibility
|
||||||
message: string;
|
message: string;
|
||||||
rawContentState: RawDraftContentState;
|
value: Value;
|
||||||
format: MessageFormat = 'html';
|
format: MessageFormat = 'rich';
|
||||||
|
|
||||||
constructor(contentState: ?ContentState, format: ?MessageFormat) {
|
constructor(value: ?Value, format: ?MessageFormat) {
|
||||||
this.rawContentState = contentState ? convertToRaw(contentState) : null;
|
this.rawContentState = contentState ? convertToRaw(contentState) : null;
|
||||||
this.format = format;
|
this.format = format;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toContentState(outputFormat: MessageFormat): ContentState {
|
toValue(outputFormat: MessageFormat): Value {
|
||||||
const contentState = convertFromRaw(this.rawContentState);
|
|
||||||
if (outputFormat === 'markdown') {
|
if (outputFormat === 'markdown') {
|
||||||
if (this.format === 'html') {
|
if (this.format === 'rich') {
|
||||||
return ContentState.createFromText(RichText.stateToMarkdown(contentState));
|
// convert a rich formatted history entry to its MD equivalent
|
||||||
|
const markdown = new Markdown({});
|
||||||
|
return new Value({ data: markdown.serialize(value) });
|
||||||
|
// return ContentState.createFromText(RichText.stateToMarkdown(contentState));
|
||||||
}
|
}
|
||||||
} else {
|
else if (this.format === 'markdown') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
} else if (outputFormat === 'rich') {
|
||||||
if (this.format === 'markdown') {
|
if (this.format === 'markdown') {
|
||||||
return RichText.htmlToContentState(new Markdown(contentState.getPlainText()).toHTML());
|
// convert MD formatted string to its rich equivalent.
|
||||||
|
const plain = new Plain({});
|
||||||
|
const md = new Md({});
|
||||||
|
return md.deserialize(plain.serialize(value));
|
||||||
|
// return RichText.htmlToContentState(new Markdown(contentState.getPlainText()).toHTML());
|
||||||
|
}
|
||||||
|
else if (this.format === 'rich') {
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// history item has format === outputFormat
|
log.error("unknown format -> outputFormat conversion");
|
||||||
return contentState;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,16 +86,16 @@ export default class ComposerHistoryManager {
|
||||||
this.lastIndex = this.currentIndex;
|
this.lastIndex = this.currentIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
save(contentState: ContentState, format: MessageFormat) {
|
save(value: Value, format: MessageFormat) {
|
||||||
const item = new HistoryItem(contentState, format);
|
const item = new HistoryItem(value, format);
|
||||||
this.history.push(item);
|
this.history.push(item);
|
||||||
this.currentIndex = this.lastIndex + 1;
|
this.currentIndex = this.lastIndex + 1;
|
||||||
sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item));
|
sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
getItem(offset: number, format: MessageFormat): ?ContentState {
|
getItem(offset: number, format: MessageFormat): ?Value {
|
||||||
this.currentIndex = _clamp(this.currentIndex + offset, 0, this.lastIndex - 1);
|
this.currentIndex = _clamp(this.currentIndex + offset, 0, this.lastIndex - 1);
|
||||||
const item = this.history[this.currentIndex];
|
const item = this.history[this.currentIndex];
|
||||||
return item ? item.toContentState(format) : null;
|
return item ? item.toValue(format) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,9 +18,16 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import type SyntheticKeyboardEvent from 'react/lib/SyntheticKeyboardEvent';
|
import type SyntheticKeyboardEvent from 'react/lib/SyntheticKeyboardEvent';
|
||||||
|
|
||||||
import {Editor, EditorState, RichUtils, CompositeDecorator, Modifier,
|
import { Editor } from 'slate-react';
|
||||||
getDefaultKeyBinding, KeyBindingUtil, ContentState, ContentBlock, SelectionState,
|
import { Value } from 'slate';
|
||||||
Entity} from 'draft-js';
|
|
||||||
|
import Html from 'slate-html-serializer';
|
||||||
|
import Markdown as Md from 'slate-md-serializer';
|
||||||
|
import Plain from 'slate-plain-serializer';
|
||||||
|
|
||||||
|
// import {Editor, EditorState, RichUtils, CompositeDecorator, Modifier,
|
||||||
|
// getDefaultKeyBinding, KeyBindingUtil, ContentState, ContentBlock, SelectionState,
|
||||||
|
// Entity} from 'draft-js';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import escape from 'lodash/escape';
|
import escape from 'lodash/escape';
|
||||||
|
@ -61,20 +68,10 @@ const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
|
||||||
|
|
||||||
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
|
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
|
||||||
|
|
||||||
const ZWS_CODE = 8203;
|
|
||||||
const ZWS = String.fromCharCode(ZWS_CODE); // zero width space
|
|
||||||
|
|
||||||
const ENTITY_TYPES = {
|
const ENTITY_TYPES = {
|
||||||
AT_ROOM_PILL: 'ATROOMPILL',
|
AT_ROOM_PILL: 'ATROOMPILL',
|
||||||
};
|
};
|
||||||
|
|
||||||
function stateToMarkdown(state) {
|
|
||||||
return __stateToMarkdown(state)
|
|
||||||
.replace(
|
|
||||||
ZWS, // draft-js-export-markdown adds these
|
|
||||||
''); // this is *not* a zero width space, trust me :)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSendMessageFailed(err, room) {
|
function onSendMessageFailed(err, room) {
|
||||||
// XXX: temporary logging to try to diagnose
|
// XXX: temporary logging to try to diagnose
|
||||||
// https://github.com/vector-im/riot-web/issues/3148
|
// https://github.com/vector-im/riot-web/issues/3148
|
||||||
|
@ -103,8 +100,6 @@ export default class MessageComposerInput extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
static getKeyBinding(ev: SyntheticKeyboardEvent): string {
|
static getKeyBinding(ev: SyntheticKeyboardEvent): string {
|
||||||
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
|
||||||
|
|
||||||
// Restrict a subset of key bindings to ONLY having ctrl/meta* pressed and
|
// Restrict a subset of key bindings to ONLY having ctrl/meta* pressed and
|
||||||
// importantly NOT having alt, shift, meta/ctrl* pressed. draft-js does not
|
// importantly NOT having alt, shift, meta/ctrl* pressed. draft-js does not
|
||||||
// handle this in `getDefaultKeyBinding` so we do it ourselves here.
|
// handle this in `getDefaultKeyBinding` so we do it ourselves here.
|
||||||
|
@ -121,7 +116,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}[ev.keyCode];
|
}[ev.keyCode];
|
||||||
|
|
||||||
if (ctrlCmdCommand) {
|
if (ctrlCmdCommand) {
|
||||||
if (!ctrlCmdOnly) {
|
if (!isOnlyCtrlOrCmdKeyEvent(ev)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return ctrlCmdCommand;
|
return ctrlCmdCommand;
|
||||||
|
@ -145,17 +140,6 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.onAction = this.onAction.bind(this);
|
|
||||||
this.handleReturn = this.handleReturn.bind(this);
|
|
||||||
this.handleKeyCommand = this.handleKeyCommand.bind(this);
|
|
||||||
this.onEditorContentChanged = this.onEditorContentChanged.bind(this);
|
|
||||||
this.onUpArrow = this.onUpArrow.bind(this);
|
|
||||||
this.onDownArrow = this.onDownArrow.bind(this);
|
|
||||||
this.onTab = this.onTab.bind(this);
|
|
||||||
this.onEscape = this.onEscape.bind(this);
|
|
||||||
this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this);
|
|
||||||
this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
|
|
||||||
this.onTextPasted = this.onTextPasted.bind(this);
|
|
||||||
|
|
||||||
const isRichtextEnabled = SettingsStore.getValue('MessageComposerInput.isRichTextEnabled');
|
const isRichtextEnabled = SettingsStore.getValue('MessageComposerInput.isRichTextEnabled');
|
||||||
|
|
||||||
|
@ -185,6 +169,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
this.client = MatrixClientPeg.get();
|
this.client = MatrixClientPeg.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
findPillEntities(contentState: ContentState, contentBlock: ContentBlock, callback) {
|
findPillEntities(contentState: ContentState, contentBlock: ContentBlock, callback) {
|
||||||
contentBlock.findEntityRanges(
|
contentBlock.findEntityRanges(
|
||||||
(character) => {
|
(character) => {
|
||||||
|
@ -199,13 +184,15 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}, callback,
|
}, callback,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* "Does the right thing" to create an EditorState, based on:
|
* "Does the right thing" to create an Editor value, based on:
|
||||||
* - 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, contentState: ?ContentState): EditorState {
|
createEditorState(richText: boolean, value: ?Value): Value {
|
||||||
|
/*
|
||||||
const decorators = richText ? RichText.getScopedRTDecorators(this.props) :
|
const decorators = richText ? RichText.getScopedRTDecorators(this.props) :
|
||||||
RichText.getScopedMDDecorators(this.props);
|
RichText.getScopedMDDecorators(this.props);
|
||||||
const shouldShowPillAvatar = !SettingsStore.getValue("Pill.shouldHidePillAvatar");
|
const shouldShowPillAvatar = !SettingsStore.getValue("Pill.shouldHidePillAvatar");
|
||||||
|
@ -239,7 +226,6 @@ export default class MessageComposerInput extends React.Component {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const compositeDecorator = new CompositeDecorator(decorators);
|
const compositeDecorator = new CompositeDecorator(decorators);
|
||||||
|
|
||||||
let editorState = null;
|
let editorState = null;
|
||||||
if (contentState) {
|
if (contentState) {
|
||||||
editorState = EditorState.createWithContent(contentState, compositeDecorator);
|
editorState = EditorState.createWithContent(contentState, compositeDecorator);
|
||||||
|
@ -248,6 +234,8 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return EditorState.moveFocusToEnd(editorState);
|
return EditorState.moveFocusToEnd(editorState);
|
||||||
|
*/
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -260,12 +248,14 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUpdate(nextProps, nextState) {
|
componentWillUpdate(nextProps, nextState) {
|
||||||
|
/*
|
||||||
// this is dirty, but moving all this state to MessageComposer is dirtier
|
// this is dirty, but moving all this state to MessageComposer is dirtier
|
||||||
if (this.props.onInputStateChanged && nextState !== this.state) {
|
if (this.props.onInputStateChanged && nextState !== this.state) {
|
||||||
const state = this.getSelectionInfo(nextState.editorState);
|
const state = this.getSelectionInfo(nextState.editorState);
|
||||||
state.isRichtextEnabled = nextState.isRichtextEnabled;
|
state.isRichtextEnabled = nextState.isRichtextEnabled;
|
||||||
this.props.onInputStateChanged(state);
|
this.props.onInputStateChanged(state);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
onAction = (payload) => {
|
onAction = (payload) => {
|
||||||
|
@ -277,6 +267,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
case 'focus_composer':
|
case 'focus_composer':
|
||||||
editor.focus();
|
editor.focus();
|
||||||
break;
|
break;
|
||||||
|
/*
|
||||||
case 'insert_mention': {
|
case 'insert_mention': {
|
||||||
// Pretend that we've autocompleted this user because keeping two code
|
// Pretend that we've autocompleted this user because keeping two code
|
||||||
// paths for inserting a user pill is not fun
|
// paths for inserting a user pill is not fun
|
||||||
|
@ -322,6 +313,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -372,7 +364,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
stopServerTypingTimer() {
|
stopServerTypingTimer() {
|
||||||
if (this.serverTypingTimer) {
|
if (this.serverTypingTimer) {
|
||||||
clearTimeout(this.servrTypingTimer);
|
clearTimeout(this.serverTypingTimer);
|
||||||
this.serverTypingTimer = null;
|
this.serverTypingTimer = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -492,9 +484,9 @@ export default class MessageComposerInput extends React.Component {
|
||||||
// Record the editor state for this room so that it can be retrieved after
|
// Record the editor state for this room so that it can be retrieved after
|
||||||
// switching to another room and back
|
// switching to another room and back
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'content_state',
|
action: 'editor_state',
|
||||||
room_id: this.props.room.roomId,
|
room_id: this.props.room.roomId,
|
||||||
content_state: state.editorState.getCurrentContent(),
|
editor_state: state.editorState.getCurrentContent(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!state.hasOwnProperty('originalEditorState')) {
|
if (!state.hasOwnProperty('originalEditorState')) {
|
||||||
|
@ -528,28 +520,36 @@ export default class MessageComposerInput extends React.Component {
|
||||||
enableRichtext(enabled: boolean) {
|
enableRichtext(enabled: boolean) {
|
||||||
if (enabled === this.state.isRichtextEnabled) return;
|
if (enabled === this.state.isRichtextEnabled) return;
|
||||||
|
|
||||||
let contentState = null;
|
// FIXME: this conversion should be handled in the store, surely
|
||||||
|
// i.e. "convert my current composer value into Rich or MD, as ComposerHistoryManager already does"
|
||||||
|
|
||||||
|
let value = null;
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
const md = new Markdown(this.state.editorState.getCurrentContent().getPlainText());
|
// const md = new Markdown(this.state.editorState.getCurrentContent().getPlainText());
|
||||||
contentState = RichText.htmlToContentState(md.toHTML());
|
// contentState = RichText.htmlToContentState(md.toHTML());
|
||||||
|
|
||||||
|
const plain = new Plain({});
|
||||||
|
const md = new Md({});
|
||||||
|
value = md.deserialize(plain.serialize(this.state.editorState));
|
||||||
} else {
|
} else {
|
||||||
let markdown = RichText.stateToMarkdown(this.state.editorState.getCurrentContent());
|
// let markdown = RichText.stateToMarkdown(this.state.editorState.getCurrentContent());
|
||||||
if (markdown[markdown.length - 1] === '\n') {
|
// value = ContentState.createFromText(markdown);
|
||||||
markdown = markdown.substring(0, markdown.length - 1); // stateToMarkdown tacks on an extra newline (?!?)
|
|
||||||
}
|
const markdown = new Markdown({});
|
||||||
contentState = ContentState.createFromText(markdown);
|
value = Value({ data: markdown.serialize(value) });
|
||||||
}
|
}
|
||||||
|
|
||||||
Analytics.setRichtextMode(enabled);
|
Analytics.setRichtextMode(enabled);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
editorState: this.createEditorState(enabled, contentState),
|
editorState: this.createEditorState(enabled, value),
|
||||||
isRichtextEnabled: enabled,
|
isRichtextEnabled: enabled,
|
||||||
});
|
});
|
||||||
SettingsStore.setValue("MessageComposerInput.isRichTextEnabled", null, SettingLevel.ACCOUNT, enabled);
|
SettingsStore.setValue("MessageComposerInput.isRichTextEnabled", null, SettingLevel.ACCOUNT, enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyCommand = (command: string): boolean => {
|
handleKeyCommand = (command: string): boolean => {
|
||||||
|
/*
|
||||||
if (command === 'toggle-mode') {
|
if (command === 'toggle-mode') {
|
||||||
this.enableRichtext(!this.state.isRichtextEnabled);
|
this.enableRichtext(!this.state.isRichtextEnabled);
|
||||||
return true;
|
return true;
|
||||||
|
@ -658,11 +658,11 @@ export default class MessageComposerInput extends React.Component {
|
||||||
this.setState({editorState: newState});
|
this.setState({editorState: newState});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
/*
|
||||||
onTextPasted(text: string, html?: string) {
|
onTextPasted = (text: string, html?: string) => {
|
||||||
const currentSelection = this.state.editorState.getSelection();
|
const currentSelection = this.state.editorState.getSelection();
|
||||||
const currentContent = this.state.editorState.getCurrentContent();
|
const currentContent = this.state.editorState.getCurrentContent();
|
||||||
|
|
||||||
|
@ -682,9 +682,10 @@ export default class MessageComposerInput extends React.Component {
|
||||||
newEditorState = EditorState.forceSelection(newEditorState, contentState.getSelectionAfter());
|
newEditorState = EditorState.forceSelection(newEditorState, contentState.getSelectionAfter());
|
||||||
this.onEditorContentChanged(newEditorState);
|
this.onEditorContentChanged(newEditorState);
|
||||||
return true;
|
return true;
|
||||||
}
|
};
|
||||||
|
*/
|
||||||
handleReturn(ev) {
|
handleReturn = (ev) => {
|
||||||
|
/*
|
||||||
if (ev.shiftKey) {
|
if (ev.shiftKey) {
|
||||||
this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState));
|
this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState));
|
||||||
return true;
|
return true;
|
||||||
|
@ -701,15 +702,21 @@ export default class MessageComposerInput extends React.Component {
|
||||||
// See handleKeyCommand (when command === 'backspace')
|
// See handleKeyCommand (when command === 'backspace')
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
const contentState = this.state.editorState.getCurrentContent();
|
const contentState = this.state.editorState;
|
||||||
|
/*
|
||||||
if (!contentState.hasText()) {
|
if (!contentState.hasText()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
const plain = new Plain({});
|
||||||
|
value = md.deserialize();
|
||||||
|
|
||||||
let contentText = contentState.getPlainText(), contentHTML;
|
let contentText = plain.serialize(contentState);
|
||||||
|
let contentHTML;
|
||||||
|
|
||||||
|
/*
|
||||||
// Strip MD user (tab-completed) mentions to preserve plaintext mention behaviour.
|
// Strip MD user (tab-completed) mentions to preserve plaintext mention behaviour.
|
||||||
// We have to do this now as opposed to after calculating the contentText for MD
|
// We have to do this now as opposed to after calculating the contentText for MD
|
||||||
// mode because entity positions may not be maintained when using
|
// mode because entity positions may not be maintained when using
|
||||||
|
@ -720,10 +727,12 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
// Some commands (/join) require pills to be replaced with their text content
|
// Some commands (/join) require pills to be replaced with their text content
|
||||||
const commandText = this.removeMDLinks(contentState, ['#']);
|
const commandText = this.removeMDLinks(contentState, ['#']);
|
||||||
|
*/
|
||||||
|
const commandText = contentText;
|
||||||
const cmd = SlashCommands.processInput(this.props.room.roomId, commandText);
|
const cmd = SlashCommands.processInput(this.props.room.roomId, commandText);
|
||||||
if (cmd) {
|
if (cmd) {
|
||||||
if (!cmd.error) {
|
if (!cmd.error) {
|
||||||
this.historyManager.save(contentState, this.state.isRichtextEnabled ? 'html' : 'markdown');
|
this.historyManager.save(contentState, this.state.isRichtextEnabled ? 'rich' : 'markdown');
|
||||||
this.setState({
|
this.setState({
|
||||||
editorState: this.createEditorState(),
|
editorState: this.createEditorState(),
|
||||||
});
|
});
|
||||||
|
@ -754,6 +763,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
const quotingEv = RoomViewStore.getQuotingEvent();
|
const quotingEv = RoomViewStore.getQuotingEvent();
|
||||||
|
|
||||||
if (this.state.isRichtextEnabled) {
|
if (this.state.isRichtextEnabled) {
|
||||||
|
/*
|
||||||
// We should only send HTML if any block is styled or contains inline style
|
// We should only send HTML if any block is styled or contains inline style
|
||||||
let shouldSendHTML = false;
|
let shouldSendHTML = false;
|
||||||
|
|
||||||
|
@ -788,6 +798,8 @@ export default class MessageComposerInput extends React.Component {
|
||||||
});
|
});
|
||||||
shouldSendHTML = hasLink;
|
shouldSendHTML = hasLink;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
let shouldSendHTML = true;
|
||||||
if (shouldSendHTML) {
|
if (shouldSendHTML) {
|
||||||
contentHTML = HtmlUtils.processHtmlForSending(
|
contentHTML = HtmlUtils.processHtmlForSending(
|
||||||
RichText.contentStateToHTML(contentState),
|
RichText.contentStateToHTML(contentState),
|
||||||
|
@ -797,6 +809,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
// Use the original contentState because `contentText` has had mentions
|
// Use the original contentState because `contentText` has had mentions
|
||||||
// stripped and these need to end up in contentHTML.
|
// stripped and these need to end up in contentHTML.
|
||||||
|
|
||||||
|
/*
|
||||||
// Replace all Entities of type `LINK` with markdown link equivalents.
|
// Replace all Entities of type `LINK` with markdown link equivalents.
|
||||||
// TODO: move this into `Markdown` and do the same conversion in the other
|
// TODO: move this into `Markdown` and do the same conversion in the other
|
||||||
// two places (toggling from MD->RT mode and loading MD history into RT mode)
|
// two places (toggling from MD->RT mode and loading MD history into RT mode)
|
||||||
|
@ -817,7 +830,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
});
|
});
|
||||||
return blockText;
|
return blockText;
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
|
*/
|
||||||
const md = new Markdown(pt);
|
const md = new Markdown(pt);
|
||||||
// if contains no HTML and we're not quoting (needing HTML)
|
// if contains no HTML and we're not quoting (needing HTML)
|
||||||
if (md.isPlainText() && !quotingEv) {
|
if (md.isPlainText() && !quotingEv) {
|
||||||
|
@ -832,7 +845,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
this.historyManager.save(
|
this.historyManager.save(
|
||||||
contentState,
|
contentState,
|
||||||
this.state.isRichtextEnabled ? 'html' : 'markdown',
|
this.state.isRichtextEnabled ? 'rich' : 'markdown',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (contentText.startsWith('/me')) {
|
if (contentText.startsWith('/me')) {
|
||||||
|
@ -881,7 +894,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
};
|
||||||
|
|
||||||
onUpArrow = (e) => {
|
onUpArrow = (e) => {
|
||||||
this.onVerticalArrow(e, true);
|
this.onVerticalArrow(e, true);
|
||||||
|
@ -896,6 +909,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
// Select history only if we are not currently auto-completing
|
// Select history only if we are not currently auto-completing
|
||||||
if (this.autocomplete.state.completionList.length === 0) {
|
if (this.autocomplete.state.completionList.length === 0) {
|
||||||
// Don't go back in history if we're in the middle of a multi-line message
|
// Don't go back in history if we're in the middle of a multi-line message
|
||||||
|
@ -927,8 +941,10 @@ export default class MessageComposerInput extends React.Component {
|
||||||
this.moveAutocompleteSelection(up);
|
this.moveAutocompleteSelection(up);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
selectHistory = async (up) => {
|
selectHistory = async (up) => {
|
||||||
const delta = up ? -1 : 1;
|
const delta = up ? -1 : 1;
|
||||||
|
|
||||||
|
@ -950,7 +966,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newContent = this.historyManager.getItem(delta, this.state.isRichtextEnabled ? 'html' : 'markdown');
|
const newContent = this.historyManager.getItem(delta, this.state.isRichtextEnabled ? 'rich' : 'markdown');
|
||||||
if (!newContent) return false;
|
if (!newContent) return false;
|
||||||
let editorState = EditorState.push(
|
let editorState = EditorState.push(
|
||||||
this.state.editorState,
|
this.state.editorState,
|
||||||
|
@ -969,6 +985,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
this.setState({editorState});
|
this.setState({editorState});
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
onTab = async (e) => {
|
onTab = async (e) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -1061,8 +1078,9 @@ export default class MessageComposerInput extends React.Component {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
onFormatButtonClicked(name: "bold" | "italic" | "strike" | "code" | "underline" | "quote" | "bullet" | "numbullet", e) {
|
onFormatButtonClicked = (name: "bold" | "italic" | "strike" | "code" | "underline" | "quote" | "bullet" | "numbullet", e) => {
|
||||||
e.preventDefault(); // don't steal focus from the editor!
|
e.preventDefault(); // don't steal focus from the editor!
|
||||||
|
/*
|
||||||
const command = {
|
const command = {
|
||||||
code: 'code-block',
|
code: 'code-block',
|
||||||
quote: 'blockquote',
|
quote: 'blockquote',
|
||||||
|
@ -1070,7 +1088,8 @@ export default class MessageComposerInput extends React.Component {
|
||||||
numbullet: 'ordered-list-item',
|
numbullet: 'ordered-list-item',
|
||||||
}[name] || name;
|
}[name] || name;
|
||||||
this.handleKeyCommand(command);
|
this.handleKeyCommand(command);
|
||||||
}
|
*/
|
||||||
|
};
|
||||||
|
|
||||||
/* returns inline style and block type of current SelectionState so MessageComposer can render formatting
|
/* returns inline style and block type of current SelectionState so MessageComposer can render formatting
|
||||||
buttons. */
|
buttons. */
|
||||||
|
|
|
@ -15,15 +15,16 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
import dis from '../dispatcher';
|
import dis from '../dispatcher';
|
||||||
import { Store } from 'flux/utils';
|
import { Store } from 'flux/utils';
|
||||||
import {convertToRaw, convertFromRaw} from 'draft-js';
|
|
||||||
|
|
||||||
const INITIAL_STATE = {
|
const INITIAL_STATE = {
|
||||||
editorStateMap: localStorage.getItem('content_state') ?
|
// a map of room_id to rich text editor composer state
|
||||||
JSON.parse(localStorage.getItem('content_state')) : {},
|
editorStateMap: localStorage.getItem('editor_state') ?
|
||||||
|
JSON.parse(localStorage.getItem('editor_state')) : {},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class for storing application state to do with the message composer. This is a simple
|
* A class for storing application state to do with the message composer (specifically
|
||||||
|
* in-progress message drafts). This is a simple
|
||||||
* flux store that listens for actions and updates its state accordingly, informing any
|
* flux store that listens for actions and updates its state accordingly, informing any
|
||||||
* listeners (views) of state changes.
|
* listeners (views) of state changes.
|
||||||
*/
|
*/
|
||||||
|
@ -42,7 +43,7 @@ class MessageComposerStore extends Store {
|
||||||
|
|
||||||
__onDispatch(payload) {
|
__onDispatch(payload) {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'content_state':
|
case 'editor_state':
|
||||||
this._contentState(payload);
|
this._contentState(payload);
|
||||||
break;
|
break;
|
||||||
case 'on_logged_out':
|
case 'on_logged_out':
|
||||||
|
@ -53,16 +54,15 @@ class MessageComposerStore extends Store {
|
||||||
|
|
||||||
_contentState(payload) {
|
_contentState(payload) {
|
||||||
const editorStateMap = this._state.editorStateMap;
|
const editorStateMap = this._state.editorStateMap;
|
||||||
editorStateMap[payload.room_id] = convertToRaw(payload.content_state);
|
editorStateMap[payload.room_id] = payload.editor_state;
|
||||||
localStorage.setItem('content_state', JSON.stringify(editorStateMap));
|
localStorage.setItem('editor_state', JSON.stringify(editorStateMap));
|
||||||
this._setState({
|
this._setState({
|
||||||
editorStateMap: editorStateMap,
|
editorStateMap: editorStateMap,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getContentState(roomId) {
|
getContentState(roomId) {
|
||||||
return this._state.editorStateMap[roomId] ?
|
return this._state.editorStateMap[roomId];
|
||||||
convertFromRaw(this._state.editorStateMap[roomId]) : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
|
|
Loading…
Reference in a new issue