make history work again
parent
ff42ef4a58
commit
8b2eb2c400
|
@ -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 */
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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}
|
||||||
|
|
Loading…
Reference in New Issue