From 084a933dbdd54fcfdc2186249bf5ef563e57b648 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 5 Jul 2017 10:24:55 +0100 Subject: [PATCH 1/3] Implement MessageComposerStore to persist composer state across room switching This behaviour was present in the old composer but implemented using local storage. This is unecessary as we don't really care about our drafts across clients, the important thing is that our draft is kept when switching rooms. As a bonus, ifnore vertical arrow key presses when a modifier key is pressed so that the room switching keys (alt + up/down arrow) don't also cause history browsing (or autocomplete browsing). --- .../views/rooms/MessageComposerInput.js | 19 +++++ src/stores/MessageComposerStore.js | 73 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/stores/MessageComposerStore.js diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 818c108211..01e836765a 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -43,6 +43,8 @@ import Markdown from '../../../Markdown'; import ComposerHistoryManager from '../../../ComposerHistoryManager'; import {onSendMessageFailed} from './MessageComposerInputOld'; +import MessageComposerStore from '../../../stores/MessageComposerStore'; + const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000; const ZWS_CODE = 8203; @@ -170,6 +172,11 @@ export default class MessageComposerInput extends React.Component { componentDidMount() { this.dispatcherRef = dis.register(this.onAction); this.historyManager = new ComposerHistoryManager(this.props.room.roomId); + + // Reinstate the editor state for this room + this.setState({ + editorState: MessageComposerStore.getEditorState(this.props.room.roomId), + }); } componentWillUnmount() { @@ -336,6 +343,14 @@ export default class MessageComposerInput extends React.Component { this.onFinishedTyping(); } + // Record the editor state for this room so that it can be retrieved after + // switching to another room and back + dis.dispatch({ + action: 'editor_state', + room_id: this.props.room.roomId, + editor_state: state.editorState, + }); + if (!state.hasOwnProperty('originalEditorState')) { state.originalEditorState = null; } @@ -632,6 +647,10 @@ export default class MessageComposerInput extends React.Component { }; onVerticalArrow = (e, up) => { + if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) { + return; + } + // Select history only if we are not currently auto-completing if (this.autocomplete.state.completionList.length === 0) { // Don't go back in history if we're in the middle of a multi-line message diff --git a/src/stores/MessageComposerStore.js b/src/stores/MessageComposerStore.js new file mode 100644 index 0000000000..1e83105300 --- /dev/null +++ b/src/stores/MessageComposerStore.js @@ -0,0 +1,73 @@ +/* +Copyright 2017 Vector Creations Ltd + +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 dis from '../dispatcher'; +import {Store} from 'flux/utils'; + +const INITIAL_STATE = { + editorStateMap: {}, +}; + +/** + * A class for storing application state to do with the message composer. This is a simple + * flux store that listens for actions and updates its state accordingly, informing any + * listeners (views) of state changes. + */ +class MessageComposerStore extends Store { + constructor() { + super(dis); + + // Initialise state + this._state = INITIAL_STATE; + } + + _setState(newState) { + this._state = Object.assign(this._state, newState); + this.__emitChange(); + } + + __onDispatch(payload) { + switch (payload.action) { + case 'editor_state': + this._editorState(payload); + break; + case 'on_logged_out': + this.reset(); + break; + } + } + + _editorState(payload) { + const editorStateMap = this._state.editorStateMap; + editorStateMap[payload.room_id] = payload.editor_state; + this._setState({ + editorStateMap: editorStateMap, + }); + } + + getEditorState(roomId) { + return this._state.editorStateMap[roomId]; + } + + reset() { + this._state = Object.assign({}, INITIAL_STATE); + } +} + +let singletonMessageComposerStore = null; +if (!singletonMessageComposerStore) { + singletonMessageComposerStore = new MessageComposerStore(); +} +module.exports = singletonMessageComposerStore; From 3d5b3ed7adc330b72f13124a5aa3840f4a06c5ec Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 5 Jul 2017 11:49:34 +0100 Subject: [PATCH 2/3] Use ContentState instead and persist over localStorage --- .../views/rooms/MessageComposerInput.js | 18 ++++++------------ src/stores/MessageComposerStore.js | 19 ++++++++++++------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 01e836765a..965b954233 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -132,7 +132,10 @@ export default class MessageComposerInput extends React.Component { isRichtextEnabled, // the currently displayed editor state (note: this is always what is modified on input) - editorState: null, + editorState: this.createEditorState( + isRichtextEnabled, + MessageComposerStore.getContentState(this.props.room.roomId), + ), // the original editor state, before we started tabbing through completions originalEditorState: null, @@ -142,10 +145,6 @@ export default class MessageComposerInput extends React.Component { currentlyComposedEditorState: null, }; - // bit of a hack, but we need to do this here since createEditorState needs isRichtextEnabled - /* eslint react/no-direct-mutation-state:0 */ - this.state.editorState = this.createEditorState(); - this.client = MatrixClientPeg.get(); } @@ -172,11 +171,6 @@ export default class MessageComposerInput extends React.Component { componentDidMount() { this.dispatcherRef = dis.register(this.onAction); this.historyManager = new ComposerHistoryManager(this.props.room.roomId); - - // Reinstate the editor state for this room - this.setState({ - editorState: MessageComposerStore.getEditorState(this.props.room.roomId), - }); } componentWillUnmount() { @@ -346,9 +340,9 @@ export default class MessageComposerInput extends React.Component { // Record the editor state for this room so that it can be retrieved after // switching to another room and back dis.dispatch({ - action: 'editor_state', + action: 'content_state', room_id: this.props.room.roomId, - editor_state: state.editorState, + content_state: state.editorState.getCurrentContent(), }); if (!state.hasOwnProperty('originalEditorState')) { diff --git a/src/stores/MessageComposerStore.js b/src/stores/MessageComposerStore.js index 1e83105300..ac48c522e4 100644 --- a/src/stores/MessageComposerStore.js +++ b/src/stores/MessageComposerStore.js @@ -15,9 +15,11 @@ limitations under the License. */ import dis from '../dispatcher'; import {Store} from 'flux/utils'; +import {convertToRaw, convertFromRaw} from 'draft-js'; const INITIAL_STATE = { - editorStateMap: {}, + editorStateMap: localStorage.getItem('content_state') ? + JSON.parse(localStorage.getItem('content_state')) : {}, }; /** @@ -40,8 +42,8 @@ class MessageComposerStore extends Store { __onDispatch(payload) { switch (payload.action) { - case 'editor_state': - this._editorState(payload); + case 'content_state': + this._contentState(payload); break; case 'on_logged_out': this.reset(); @@ -49,16 +51,19 @@ class MessageComposerStore extends Store { } } - _editorState(payload) { + _contentState(payload) { const editorStateMap = this._state.editorStateMap; - editorStateMap[payload.room_id] = payload.editor_state; + editorStateMap[payload.room_id] = convertToRaw(payload.content_state); + localStorage.setItem('content_state', JSON.stringify(editorStateMap)); + console.info(localStorage.getItem('content_state')); this._setState({ editorStateMap: editorStateMap, }); } - getEditorState(roomId) { - return this._state.editorStateMap[roomId]; + getContentState(roomId) { + return this._state.editorStateMap[roomId] ? + convertFromRaw(this._state.editorStateMap[roomId]) : null; } reset() { From df23a6cd85183aa122821c9bb93223b9786a2330 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 5 Jul 2017 13:38:34 +0100 Subject: [PATCH 3/3] Use Object.assign to set initial state of MessageComposerStore Otherwise we just modify the initial state when running --- src/stores/MessageComposerStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/MessageComposerStore.js b/src/stores/MessageComposerStore.js index ac48c522e4..6e13c8f825 100644 --- a/src/stores/MessageComposerStore.js +++ b/src/stores/MessageComposerStore.js @@ -32,7 +32,7 @@ class MessageComposerStore extends Store { super(dis); // Initialise state - this._state = INITIAL_STATE; + this._state = Object.assign({}, INITIAL_STATE); } _setState(newState) {