diff --git a/src/ComposerHistoryManager.js b/src/ComposerHistoryManager.js index 1b3fb588eb..33030ed6cf 100644 --- a/src/ComposerHistoryManager.js +++ b/src/ComposerHistoryManager.js @@ -15,38 +15,8 @@ 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 = []; prefix: string; @@ -57,26 +27,30 @@ export default class ComposerHistoryManager { this.prefix = prefix + roomId; // TODO: Performance issues? - let item; - for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) { + let index = 0; + let itemJSON; + + while (itemJSON = sessionStorage.getItem(`${this.prefix}[${index}]`)) { try { - this.history.push( - HistoryItem.fromJSON(JSON.parse(item)), - ); + const serializedParts = JSON.parse(itemJSON); + this.history.push(serializedParts); } catch (e) { console.warn("Throwing away unserialisable history", e); + break; } + ++index; } - this.lastIndex = this.currentIndex; + this.lastIndex = this.history.length - 1; // reset currentIndex to account for any unserialisable history - this.currentIndex = this.history.length; + this.currentIndex = this.lastIndex + 1; } - save(value: Value, format: MessageFormat) { - const item = new HistoryItem(value, format); - this.history.push(item); + save(editorModel: Object) { + const serializedParts = editorModel.serializeParts(); + this.history.push(serializedParts); this.currentIndex = this.history.length; - sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item.toJSON())); + this.lastIndex += 1; + sessionStorage.setItem(`${this.prefix}[${this.lastIndex}]`, JSON.stringify(serializedParts)); } getItem(offset: number): ?HistoryItem { diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index ccee439237..92abefa117 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -144,6 +144,10 @@ export default class BasicMessageEditor extends React.Component { return this._lastCaret; } + isSelectionCollapsed() { + return !this._lastSelection || this._lastSelection.isCollapsed; + } + isCaretAtStart() { return this.getCaret().offset === 0; } diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 3b65b3e83d..a400433aef 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -27,6 +27,8 @@ import ReplyPreview from "./ReplyPreview"; import RoomViewStore from '../../../stores/RoomViewStore'; import ReplyThread from "../elements/ReplyThread"; import {parseEvent} from '../../../editor/deserialize'; +import {findEditableEvent} from '../../../utils/EventUtils'; +import ComposerHistoryManager from "../../../ComposerHistoryManager"; function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); @@ -79,6 +81,7 @@ export default class SendMessageComposer extends React.Component { super(props, context); this.model = null; this._editorRef = null; + this.currentlyComposedEditorState = null; } _setEditorRef = ref => { @@ -86,19 +89,75 @@ export default class SendMessageComposer extends React.Component { }; _onKeyDown = (event) => { - if (event.metaKey || event.altKey || event.shiftKey) { - return; - } - if (event.key === "Enter") { + const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey; + if (event.key === "Enter" && !hasModifier) { this._sendMessage(); event.preventDefault(); + } else if (event.key === "ArrowUp") { + this.onVerticalArrow(event, true); + } else if (event.key === "ArrowDown") { + this.onVerticalArrow(event, false); + } + } + + onVerticalArrow(e, up) { + if (e.ctrlKey || e.shiftKey || e.metaKey) return; + + const shouldSelectHistory = e.altKey; + const shouldEditLastMessage = !e.altKey && up && !RoomViewStore.getQuotingEvent(); + + if (shouldSelectHistory) { + // Try select composer history + const selected = this.selectSendHistory(up); + if (selected) { + // We're selecting history, so prevent the key event from doing anything else + e.preventDefault(); + } + } else if (shouldEditLastMessage) { + // selection must be collapsed and caret at start + if (this._editorRef.isSelectionCollapsed() && this._editorRef.isCaretAtStart()) { + 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, + }); + } + } + } + } + + selectSendHistory(up) { + const delta = up ? -1 : 1; + + // True if we are not currently selecting history, but composing a message + if (this.sendHistoryManager.currentIndex === this.sendHistoryManager.history.length) { + // We can't go any further - there isn't any more history, so nop. + if (!up) { + return; + } + this.currentlyComposedEditorState = this.model.serializeParts(); + } else if (this.sendHistoryManager.currentIndex + delta === this.sendHistoryManager.history.length) { + // True when we return to the message being composed currently + this.model.reset(this.currentlyComposedEditorState); + this.sendHistoryManager.currentIndex = this.sendHistoryManager.history.length; + return; + } + const serializedParts = this.sendHistoryManager.getItem(delta); + if (serializedParts) { + this.model.reset(serializedParts); + this._editorRef.focus(); } } _sendMessage() { const isReply = !!RoomViewStore.getQuotingEvent(); const {roomId} = this.props.room; - this.context.matrixClient.sendMessage(roomId, createMessageContent(this.model, this.props.permalinkCreator)); + const content = createMessageContent(this.model, this.props.permalinkCreator); + this.context.matrixClient.sendMessage(roomId, content); + this.sendHistoryManager.save(this.model); this.model.reset([]); this._editorRef.clearUndoHistory(); @@ -125,6 +184,7 @@ export default class SendMessageComposer extends React.Component { const partCreator = new PartCreator(this.props.room, this.context.matrixClient); this.model = new EditorModel([], partCreator); this.dispatcherRef = dis.register(this.onAction); + this.sendHistoryManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_'); } onAction = (payload) => {