bring back composer send history and arrow up to edit previous message

pull/21833/head
Bruno Windels 2019-08-20 17:18:46 +02:00
parent ca3539d53e
commit cc82353d8f
3 changed files with 84 additions and 46 deletions

View File

@ -15,38 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {Value} from 'slate';
import _clamp from 'lodash/clamp'; 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 { export default class ComposerHistoryManager {
history: Array<HistoryItem> = []; history: Array<HistoryItem> = [];
prefix: string; prefix: string;
@ -57,26 +27,30 @@ export default class ComposerHistoryManager {
this.prefix = prefix + roomId; this.prefix = prefix + roomId;
// TODO: Performance issues? // TODO: Performance issues?
let item; let index = 0;
for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) { let itemJSON;
while (itemJSON = sessionStorage.getItem(`${this.prefix}[${index}]`)) {
try { try {
this.history.push( const serializedParts = JSON.parse(itemJSON);
HistoryItem.fromJSON(JSON.parse(item)), this.history.push(serializedParts);
);
} catch (e) { } catch (e) {
console.warn("Throwing away unserialisable history", 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 // reset currentIndex to account for any unserialisable history
this.currentIndex = this.history.length; this.currentIndex = this.lastIndex + 1;
} }
save(value: Value, format: MessageFormat) { save(editorModel: Object) {
const item = new HistoryItem(value, format); const serializedParts = editorModel.serializeParts();
this.history.push(item); this.history.push(serializedParts);
this.currentIndex = this.history.length; 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 { getItem(offset: number): ?HistoryItem {

View File

@ -144,6 +144,10 @@ export default class BasicMessageEditor extends React.Component {
return this._lastCaret; return this._lastCaret;
} }
isSelectionCollapsed() {
return !this._lastSelection || this._lastSelection.isCollapsed;
}
isCaretAtStart() { isCaretAtStart() {
return this.getCaret().offset === 0; return this.getCaret().offset === 0;
} }

View File

@ -27,6 +27,8 @@ import ReplyPreview from "./ReplyPreview";
import RoomViewStore from '../../../stores/RoomViewStore'; import RoomViewStore from '../../../stores/RoomViewStore';
import ReplyThread from "../elements/ReplyThread"; import ReplyThread from "../elements/ReplyThread";
import {parseEvent} from '../../../editor/deserialize'; import {parseEvent} from '../../../editor/deserialize';
import {findEditableEvent} from '../../../utils/EventUtils';
import ComposerHistoryManager from "../../../ComposerHistoryManager";
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
@ -79,6 +81,7 @@ export default class SendMessageComposer extends React.Component {
super(props, context); super(props, context);
this.model = null; this.model = null;
this._editorRef = null; this._editorRef = null;
this.currentlyComposedEditorState = null;
} }
_setEditorRef = ref => { _setEditorRef = ref => {
@ -86,19 +89,75 @@ export default class SendMessageComposer extends React.Component {
}; };
_onKeyDown = (event) => { _onKeyDown = (event) => {
if (event.metaKey || event.altKey || event.shiftKey) { const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
return; if (event.key === "Enter" && !hasModifier) {
}
if (event.key === "Enter") {
this._sendMessage(); this._sendMessage();
event.preventDefault(); 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() { _sendMessage() {
const isReply = !!RoomViewStore.getQuotingEvent(); const isReply = !!RoomViewStore.getQuotingEvent();
const {roomId} = this.props.room; 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.model.reset([]);
this._editorRef.clearUndoHistory(); this._editorRef.clearUndoHistory();
@ -125,6 +184,7 @@ export default class SendMessageComposer extends React.Component {
const partCreator = new PartCreator(this.props.room, this.context.matrixClient); const partCreator = new PartCreator(this.props.room, this.context.matrixClient);
this.model = new EditorModel([], partCreator); this.model = new EditorModel([], partCreator);
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
this.sendHistoryManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_');
} }
onAction = (payload) => { onAction = (payload) => {