make history work again

pull/21833/head
Matthew Hodgson 2018-05-08 01:54:06 +01:00
parent ff42ef4a58
commit 8b2eb2c400
3 changed files with 78 additions and 67 deletions

View File

@ -78,7 +78,7 @@ limitations under the License.
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 60px; min-height: 60px;
justify-content: center; justify-content: start;
align-items: flex-start; align-items: flex-start;
font-size: 14px; font-size: 14px;
margin-right: 6px; margin-right: 6px;
@ -86,9 +86,8 @@ limitations under the License.
.mx_MessageComposer_editor { .mx_MessageComposer_editor {
width: 100%; width: 100%;
flex: 1;
max-height: 120px; max-height: 120px;
min-height: 21px; min-height: 19px;
overflow: auto; overflow: auto;
} }
@ -106,6 +105,7 @@ limitations under the License.
display: none; display: none;
} }
/*
.mx_MessageComposer_input .DraftEditor-root { .mx_MessageComposer_input .DraftEditor-root {
width: 100%; width: 100%;
flex: 1; flex: 1;
@ -114,6 +114,7 @@ limitations under the License.
min-height: 21px; min-height: 21px;
overflow: auto; overflow: auto;
} }
*/
.mx_MessageComposer_input .DraftEditor-root .DraftEditor-editorContainer { .mx_MessageComposer_input .DraftEditor-root .DraftEditor-editorContainer {
/* Ensure mx_UserPill and mx_RoomPill (see _RichText) are not obscured from the top */ /* Ensure mx_UserPill and mx_RoomPill (see _RichText) are not obscured from the top */

View File

@ -38,28 +38,44 @@ class HistoryItem {
this.format = format; this.format = format;
} }
static fromJSON(obj): HistoryItem {
return new HistoryItem(
Value.fromJSON(obj.value),
obj.format
);
}
toJSON(): Object {
return {
value: this.value.toJSON(),
format: this.format
};
}
// FIXME: rather than supporting storing history in either format, why don't we pick
// one canonical form?
toValue(outputFormat: MessageFormat): Value { toValue(outputFormat: MessageFormat): Value {
if (outputFormat === 'markdown') { if (outputFormat === 'markdown') {
if (this.format === 'rich') { if (this.format === 'rich') {
// convert a rich formatted history entry to its MD equivalent // convert a rich formatted history entry to its MD equivalent
return Plain.deserialize(Md.serialize(value)); return Plain.deserialize(Md.serialize(this.value));
// return ContentState.createFromText(RichText.stateToMarkdown(contentState)); // return ContentState.createFromText(RichText.stateToMarkdown(contentState));
} }
else if (this.format === 'markdown') { else if (this.format === 'markdown') {
return value; return this.value;
} }
} else if (outputFormat === 'rich') { } else if (outputFormat === 'rich') {
if (this.format === 'markdown') { if (this.format === 'markdown') {
// convert MD formatted string to its rich equivalent. // convert MD formatted string to its rich equivalent.
return Md.deserialize(Plain.serialize(value)); return Md.deserialize(Plain.serialize(this.value));
// return RichText.htmlToContentState(new Markdown(contentState.getPlainText()).toHTML()); // return RichText.htmlToContentState(new Markdown(contentState.getPlainText()).toHTML());
} }
else if (this.format === 'rich') { else if (this.format === 'rich') {
return value; return this.value;
} }
} }
log.error("unknown format -> outputFormat conversion"); log.error("unknown format -> outputFormat conversion");
return value; return this.value;
} }
} }
@ -76,7 +92,7 @@ export default class ComposerHistoryManager {
let item; let item;
for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) { for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) {
this.history.push( this.history.push(
Object.assign(new HistoryItem(), JSON.parse(item)), HistoryItem.fromJSON(JSON.parse(item))
); );
} }
this.lastIndex = this.currentIndex; this.lastIndex = this.currentIndex;
@ -86,7 +102,7 @@ export default class ComposerHistoryManager {
const item = new HistoryItem(value, 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.toJSON()));
} }
getItem(offset: number, format: MessageFormat): ?Value { getItem(offset: number, format: MessageFormat): ?Value {

View File

@ -15,6 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import type SyntheticKeyboardEvent from 'react/lib/SyntheticKeyboardEvent'; import type SyntheticKeyboardEvent from 'react/lib/SyntheticKeyboardEvent';
@ -101,6 +102,7 @@ export default class MessageComposerInput extends React.Component {
onInputStateChanged: PropTypes.func, onInputStateChanged: PropTypes.func,
}; };
/*
static getKeyBinding(ev: SyntheticKeyboardEvent): string { static getKeyBinding(ev: SyntheticKeyboardEvent): string {
// 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
@ -135,6 +137,7 @@ export default class MessageComposerInput extends React.Component {
return null; return null;
} }
*/
client: MatrixClient; client: MatrixClient;
autocomplete: Autocomplete; autocomplete: Autocomplete;
@ -392,8 +395,7 @@ export default class MessageComposerInput extends React.Component {
} }
} }
// Called by Draft to change editor contents onChange = (change: Change) => {
onEditorContentChanged = (change: Change) => {
/* /*
editorState = RichText.attachImmutableEntitiesToEmoji(editorState); editorState = RichText.attachImmutableEntitiesToEmoji(editorState);
@ -557,17 +559,25 @@ export default class MessageComposerInput extends React.Component {
} }
onKeyDown = (ev: Event, change: Change, editor: Editor) => { onKeyDown = (ev: Event, change: Change, editor: Editor) => {
if (ev.keyCode === KeyCode.ENTER) { switch (ev.keyCode) {
return this.handleReturn(ev); case KeyCode.ENTER:
return this.handleReturn(ev);
case KeyCode.UP:
return this.onVerticalArrow(ev, true);
case KeyCode.DOWN:
return this.onVerticalArrow(ev, false);
default:
// don't intercept it
return;
} }
} }
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;
} }
/*
let newState: ?EditorState = null; let newState: ?EditorState = null;
// Draft handles rich text mode commands by default but we need to do it ourselves for Markdown. // Draft handles rich text mode commands by default but we need to do it ourselves for Markdown.
@ -699,12 +709,10 @@ export default class MessageComposerInput extends React.Component {
}; };
*/ */
handleReturn = (ev) => { handleReturn = (ev) => {
/*
if (ev.shiftKey) { if (ev.shiftKey) {
this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState)); return;
return true;
} }
/*
const currentBlockType = RichUtils.getCurrentBlockType(this.state.editorState); const currentBlockType = RichUtils.getCurrentBlockType(this.state.editorState);
if ( if (
['code-block', 'blockquote', 'unordered-list-item', 'ordered-list-item'] ['code-block', 'blockquote', 'unordered-list-item', 'ordered-list-item']
@ -718,15 +726,12 @@ export default class MessageComposerInput extends React.Component {
} }
*/ */
const contentState = this.state.editorState; const contentState = this.state.editorState;
/*
if (!contentState.hasText()) {
return true;
}
*/
let contentText = Plain.serialize(contentState); let contentText = Plain.serialize(contentState);
let contentHTML; let contentHTML;
if (contentText === '') return true;
/* /*
// 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
@ -914,41 +919,39 @@ export default class MessageComposerInput extends React.Component {
return true; return true;
}; };
onUpArrow = (e) => {
this.onVerticalArrow(e, true);
};
onDownArrow = (e) => {
this.onVerticalArrow(e, false);
};
onVerticalArrow = (e, up) => { onVerticalArrow = (e, up) => {
if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) { if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) {
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
const selection = this.state.editorState.getSelection();
const blockKey = selection.getStartKey();
const firstBlock = this.state.editorState.getCurrentContent().getFirstBlock();
const lastBlock = this.state.editorState.getCurrentContent().getLastBlock();
let canMoveUp = false; // determine whether our cursor is at the top or bottom of the multiline
let canMoveDown = false; // input box by just looking at the position of the plain old DOM selection.
if (blockKey === firstBlock.getKey()) { const selection = window.getSelection();
canMoveUp = selection.getStartOffset() === selection.getEndOffset() && const range = selection.getRangeAt(0);
selection.getStartOffset() === 0; const cursorRect = range.getBoundingClientRect();
const editorNode = ReactDOM.findDOMNode(this.refs.editor);
const editorRect = editorNode.getBoundingClientRect();
let navigateHistory = false;
if (up) {
let scrollCorrection = editorNode.scrollTop;
if (cursorRect.top - editorRect.top + scrollCorrection == 0) {
navigateHistory = true;
}
}
else {
let scrollCorrection =
editorNode.scrollHeight - editorNode.clientHeight - editorNode.scrollTop;
if (cursorRect.bottom - editorRect.bottom + scrollCorrection == 0) {
navigateHistory = true;
}
} }
if (blockKey === lastBlock.getKey()) { if (!navigateHistory) return;
canMoveDown = selection.getStartOffset() === selection.getEndOffset() &&
selection.getStartOffset() === lastBlock.getText().length;
}
if ((up && !canMoveUp) || (!up && !canMoveDown)) return;
const selected = this.selectHistory(up); const selected = this.selectHistory(up);
if (selected) { if (selected) {
@ -959,10 +962,8 @@ 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;
@ -984,26 +985,19 @@ export default class MessageComposerInput extends React.Component {
return; return;
} }
const newContent = this.historyManager.getItem(delta, this.state.isRichtextEnabled ? 'rich' : 'markdown'); let editorState = this.historyManager.getItem(delta, this.state.isRichtextEnabled ? 'rich' : 'markdown');
if (!newContent) return false;
let editorState = EditorState.push(
this.state.editorState,
newContent,
'insert-characters',
);
// Move selection to the end of the selected history // Move selection to the end of the selected history
let newSelection = SelectionState.createEmpty(newContent.getLastBlock().getKey()); const change = editorState.change().collapseToEndOf(editorState.document);
newSelection = newSelection.merge({ // XXX: should we be calling this.onChange(change) now?
focusOffset: newContent.getLastBlock().getLength(), // we skip it for now given we know we're about to setState anyway
anchorOffset: newContent.getLastBlock().getLength(), editorState = change.value;
});
editorState = EditorState.forceSelection(editorState, newSelection);
this.setState({editorState}); this.setState({ editorState }, ()=>{
this.refs.editor.focus();
});
return true; return true;
}; };
*/
onTab = async (e) => { onTab = async (e) => {
this.setState({ this.setState({
@ -1236,7 +1230,7 @@ export default class MessageComposerInput extends React.Component {
className="mx_MessageComposer_editor" className="mx_MessageComposer_editor"
placeholder={this.props.placeholder} placeholder={this.props.placeholder}
value={this.state.editorState} value={this.state.editorState}
onChange={this.onEditorContentChanged} onChange={this.onChange}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
/* /*
blockStyleFn={MessageComposerInput.getBlockStyle} blockStyleFn={MessageComposerInput.getBlockStyle}