Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Merge pull request #3098 from matrix-org/t3chguy/restore_composer_his…
Browse files Browse the repository at this point in the history
…tory

Restore Composer History under shift-up & down
  • Loading branch information
bwindels committed Jun 18, 2019
2 parents 5e7b456 + 03c3782 commit 32840fc
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 26 deletions.
86 changes: 86 additions & 0 deletions src/ComposerHistoryManager.js
Original file line number Diff line number Diff line change
@@ -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];
}
}
10 changes: 0 additions & 10 deletions src/components/structures/LoggedInView.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,16 +292,6 @@ const LoggedInView = React.createClass({
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);

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_DOWN:
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
Expand Down
95 changes: 79 additions & 16 deletions src/components/views/rooms/MessageComposerInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import ReplyThread from "../elements/ReplyThread";
import {ContentHelpers} from 'matrix-js-sdk';
import AccessibleButton from '../elements/AccessibleButton';
import {findEditableEvent} from '../../../utils/EventUtils';
import ComposerHistoryManager from "../../../ComposerHistoryManager";

const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');

Expand Down Expand Up @@ -140,6 +141,7 @@ export default class MessageComposerInput extends React.Component {

client: MatrixClient;
autocomplete: Autocomplete;
historyManager: ComposerHistoryManager;

constructor(props, context) {
super(props, context);
Expand Down Expand Up @@ -329,6 +331,7 @@ export default class MessageComposerInput extends React.Component {

componentWillMount() {
this.dispatcherRef = dis.register(this.onAction);
this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_');
}

componentWillUnmount() {
Expand Down Expand Up @@ -1062,6 +1065,7 @@ export default class MessageComposerInput extends React.Component {

if (cmd) {
if (!cmd.error) {
this.historyManager.save(editorState, this.state.isRichTextEnabled ? 'rich' : 'markdown');
this.setState({
editorState: this.createEditorState(),
}, ()=>{
Expand Down Expand Up @@ -1139,6 +1143,8 @@ export default class MessageComposerInput extends React.Component {
let sendHtmlFn = ContentHelpers.makeHtmlMessage;
let sendTextFn = ContentHelpers.makeTextMessage;

this.historyManager.save(editorState, this.state.isRichTextEnabled ? 'rich' : 'markdown');

if (commandText && commandText.startsWith('/me')) {
if (replyingToEv) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Expand Down Expand Up @@ -1198,31 +1204,88 @@ export default class MessageComposerInput extends React.Component {
};

onVerticalArrow = (e, up) => {
if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return;
if (e.ctrlKey || e.shiftKey || e.metaKey) return;

// Select history
const selection = this.state.editorState.selection;
if (e.altKey) {
// Try select composer history
const selected = this.selectHistory(up);
if (selected) {
// We're selecting history, so prevent the key event from doing anything else
e.preventDefault();
}
} else if (!e.altKey && up) {
// 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;
// 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 (up) {
// and we must be at the edge of the document (up=start, down=end)
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
e.preventDefault();
dis.dispatch({
action: 'edit_event',
event: editEvent,
});
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) => {
this.setState({
someCompletions: null,
Expand Down

0 comments on commit 32840fc

Please sign in to comment.