mirror of https://github.com/vector-im/riot-web
Merge pull request #3098 from matrix-org/t3chguy/restore_composer_history
Restore Composer History under shift-up & downpull/21833/head
commit
32840fc274
|
@ -0,0 +1,86 @@
|
||||||
|
//@flow
|
||||||
|
/*
|
||||||
|
Copyright 2017 Aviral Dasgupta
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Value} from 'slate';
|
||||||
|
|
||||||
|
import _clamp from 'lodash/clamp';
|
||||||
|
|
||||||
|
type MessageFormat = 'rich' | 'markdown';
|
||||||
|
|
||||||
|
class HistoryItem {
|
||||||
|
// We store history items in their native format to ensure history is accurate
|
||||||
|
// and then convert them if our RTE has subsequently changed format.
|
||||||
|
value: Value;
|
||||||
|
format: MessageFormat = 'rich';
|
||||||
|
|
||||||
|
constructor(value: ?Value, format: ?MessageFormat) {
|
||||||
|
this.value = value;
|
||||||
|
this.format = format;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJSON(obj: Object): HistoryItem {
|
||||||
|
return new HistoryItem(
|
||||||
|
Value.fromJSON(obj.value),
|
||||||
|
obj.format,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): Object {
|
||||||
|
return {
|
||||||
|
value: this.value.toJSON(),
|
||||||
|
format: this.format,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ComposerHistoryManager {
|
||||||
|
history: Array<HistoryItem> = [];
|
||||||
|
prefix: string;
|
||||||
|
lastIndex: number = 0; // used for indexing the storage
|
||||||
|
currentIndex: number = 0; // used for indexing the loaded validated history Array
|
||||||
|
|
||||||
|
constructor(roomId: string, prefix: string = 'mx_composer_history_') {
|
||||||
|
this.prefix = prefix + roomId;
|
||||||
|
|
||||||
|
// TODO: Performance issues?
|
||||||
|
let item;
|
||||||
|
for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) {
|
||||||
|
try {
|
||||||
|
this.history.push(
|
||||||
|
HistoryItem.fromJSON(JSON.parse(item)),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Throwing away unserialisable history", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.lastIndex = this.currentIndex;
|
||||||
|
// reset currentIndex to account for any unserialisable history
|
||||||
|
this.currentIndex = this.history.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
save(value: Value, format: MessageFormat) {
|
||||||
|
const item = new HistoryItem(value, format);
|
||||||
|
this.history.push(item);
|
||||||
|
this.currentIndex = this.history.length;
|
||||||
|
sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item.toJSON()));
|
||||||
|
}
|
||||||
|
|
||||||
|
getItem(offset: number): ?HistoryItem {
|
||||||
|
this.currentIndex = _clamp(this.currentIndex + offset, 0, this.history.length - 1);
|
||||||
|
return this.history[this.currentIndex];
|
||||||
|
}
|
||||||
|
}
|
|
@ -292,16 +292,6 @@ const LoggedInView = React.createClass({
|
||||||
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||||
|
|
||||||
switch (ev.keyCode) {
|
switch (ev.keyCode) {
|
||||||
case KeyCode.UP:
|
|
||||||
case KeyCode.DOWN:
|
|
||||||
if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
|
|
||||||
const action = ev.keyCode == KeyCode.UP ?
|
|
||||||
'view_prev_room' : 'view_next_room';
|
|
||||||
dis.dispatch({action: action});
|
|
||||||
handled = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyCode.PAGE_UP:
|
case KeyCode.PAGE_UP:
|
||||||
case KeyCode.PAGE_DOWN:
|
case KeyCode.PAGE_DOWN:
|
||||||
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||||
|
|
|
@ -60,6 +60,7 @@ import ReplyThread from "../elements/ReplyThread";
|
||||||
import {ContentHelpers} from 'matrix-js-sdk';
|
import {ContentHelpers} from 'matrix-js-sdk';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import {findEditableEvent} from '../../../utils/EventUtils';
|
import {findEditableEvent} from '../../../utils/EventUtils';
|
||||||
|
import ComposerHistoryManager from "../../../ComposerHistoryManager";
|
||||||
|
|
||||||
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
|
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
|
||||||
|
|
||||||
|
@ -140,6 +141,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
autocomplete: Autocomplete;
|
autocomplete: Autocomplete;
|
||||||
|
historyManager: ComposerHistoryManager;
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
@ -329,6 +331,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_');
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -1062,6 +1065,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
if (cmd) {
|
if (cmd) {
|
||||||
if (!cmd.error) {
|
if (!cmd.error) {
|
||||||
|
this.historyManager.save(editorState, this.state.isRichTextEnabled ? 'rich' : 'markdown');
|
||||||
this.setState({
|
this.setState({
|
||||||
editorState: this.createEditorState(),
|
editorState: this.createEditorState(),
|
||||||
}, ()=>{
|
}, ()=>{
|
||||||
|
@ -1139,6 +1143,8 @@ export default class MessageComposerInput extends React.Component {
|
||||||
let sendHtmlFn = ContentHelpers.makeHtmlMessage;
|
let sendHtmlFn = ContentHelpers.makeHtmlMessage;
|
||||||
let sendTextFn = ContentHelpers.makeTextMessage;
|
let sendTextFn = ContentHelpers.makeTextMessage;
|
||||||
|
|
||||||
|
this.historyManager.save(editorState, this.state.isRichTextEnabled ? 'rich' : 'markdown');
|
||||||
|
|
||||||
if (commandText && commandText.startsWith('/me')) {
|
if (commandText && commandText.startsWith('/me')) {
|
||||||
if (replyingToEv) {
|
if (replyingToEv) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
@ -1198,31 +1204,88 @@ export default class MessageComposerInput extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
onVerticalArrow = (e, up) => {
|
onVerticalArrow = (e, up) => {
|
||||||
if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return;
|
if (e.ctrlKey || e.shiftKey || e.metaKey) return;
|
||||||
|
|
||||||
// Select history
|
if (e.altKey) {
|
||||||
const selection = this.state.editorState.selection;
|
// Try select composer history
|
||||||
|
const selected = this.selectHistory(up);
|
||||||
// selection must be collapsed
|
if (selected) {
|
||||||
if (!selection.isCollapsed) return;
|
|
||||||
const document = this.state.editorState.document;
|
|
||||||
|
|
||||||
// and we must be at the edge of the document (up=start, down=end)
|
|
||||||
if (up) {
|
|
||||||
if (!selection.anchor.isAtStartOfNode(document)) return;
|
|
||||||
|
|
||||||
const editEvent = findEditableEvent(this.props.room, false);
|
|
||||||
if (editEvent) {
|
|
||||||
// We're selecting history, so prevent the key event from doing anything else
|
// We're selecting history, so prevent the key event from doing anything else
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dis.dispatch({
|
}
|
||||||
action: 'edit_event',
|
} else if (!e.altKey && up) {
|
||||||
event: editEvent,
|
// Try edit the latest message
|
||||||
});
|
const selection = this.state.editorState.selection;
|
||||||
|
|
||||||
|
// selection must be collapsed
|
||||||
|
if (!selection.isCollapsed) return;
|
||||||
|
const document = this.state.editorState.document;
|
||||||
|
|
||||||
|
// and we must be at the edge of the document (up=start, down=end)
|
||||||
|
if (!selection.anchor.isAtStartOfNode(document)) return;
|
||||||
|
|
||||||
|
if (!e.altKey) {
|
||||||
|
const editEvent = findEditableEvent(this.props.room, false);
|
||||||
|
if (editEvent) {
|
||||||
|
// We're selecting history, so prevent the key event from doing anything else
|
||||||
|
e.preventDefault();
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'edit_event',
|
||||||
|
event: editEvent,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
selectHistory = async (up) => {
|
||||||
|
const delta = up ? -1 : 1;
|
||||||
|
|
||||||
|
// True if we are not currently selecting history, but composing a message
|
||||||
|
if (this.historyManager.currentIndex === this.historyManager.history.length) {
|
||||||
|
// We can't go any further - there isn't any more history, so nop.
|
||||||
|
if (!up) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
currentlyComposedEditorState: this.state.editorState,
|
||||||
|
});
|
||||||
|
} else if (this.historyManager.currentIndex + delta === this.historyManager.history.length) {
|
||||||
|
// True when we return to the message being composed currently
|
||||||
|
this.setState({
|
||||||
|
editorState: this.state.currentlyComposedEditorState,
|
||||||
|
});
|
||||||
|
this.historyManager.currentIndex = this.historyManager.history.length;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let editorState;
|
||||||
|
const historyItem = this.historyManager.getItem(delta);
|
||||||
|
if (!historyItem) return;
|
||||||
|
|
||||||
|
if (historyItem.format === 'rich' && !this.state.isRichTextEnabled) {
|
||||||
|
editorState = this.richToMdEditorState(historyItem.value);
|
||||||
|
} else if (historyItem.format === 'markdown' && this.state.isRichTextEnabled) {
|
||||||
|
editorState = this.mdToRichEditorState(historyItem.value);
|
||||||
|
} else {
|
||||||
|
editorState = historyItem.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move selection to the end of the selected history
|
||||||
|
const change = editorState.change().moveToEndOfNode(editorState.document);
|
||||||
|
|
||||||
|
// We don't call this.onChange(change) now, as fixups on stuff like pills
|
||||||
|
// should already have been done and persisted in the history.
|
||||||
|
editorState = change.value;
|
||||||
|
|
||||||
|
this.suppressAutoComplete = true;
|
||||||
|
|
||||||
|
this.setState({ editorState }, ()=>{
|
||||||
|
this._editor.focus();
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
onTab = async (e) => {
|
onTab = async (e) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
someCompletions: null,
|
someCompletions: null,
|
||||||
|
|
Loading…
Reference in New Issue