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

Message editing: arrow key (up/down) navigation between editable events #3025

Merged
merged 10 commits into from
May 27, 2019
86 changes: 0 additions & 86 deletions src/ComposerHistoryManager.js

This file was deleted.

7 changes: 7 additions & 0 deletions src/components/structures/MessagePanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,13 @@ module.exports = React.createClass({
}
},

scrollToEventIfNeeded: function(eventId) {
const node = this.eventNodes[eventId];
if (node) {
node.scrollIntoView({block: "nearest", behavior: "instant"});
}
},

/* check the scroll state and send out pagination requests if necessary.
*/
checkFillState: function() {
Expand Down
8 changes: 7 additions & 1 deletion src/components/structures/TimelinePanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,13 @@ const TimelinePanel = React.createClass({
this.forceUpdate();
}
if (payload.action === "edit_event") {
this.setState({editEvent: payload.event});
this.setState({editEvent: payload.event}, () => {
if (payload.event && this.refs.messagePanel) {
this.refs.messagePanel.scrollToEventIfNeeded(
payload.event.getId(),
);
}
});
}
},

Expand Down
40 changes: 39 additions & 1 deletion src/components/views/elements/MessageEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import EditorModel from '../../../editor/model';
import {setCaretPosition} from '../../../editor/caret';
import {getCaretOffsetAndText} from '../../../editor/dom';
import {htmlSerializeIfNeeded, textSerialize} from '../../../editor/serialize';
import {findPreviousEditableEvent, findNextEditableEvent} from '../../../utils/EventUtils';
import {parseEvent} from '../../../editor/deserialize';
import Autocomplete from '../rooms/Autocomplete';
import {PartCreator} from '../../../editor/parts';
Expand All @@ -42,7 +43,7 @@ export default class MessageEditor extends React.Component {

constructor(props, context) {
super(props, context);
const room = this.context.matrixClient.getRoom(this.props.event.getRoomId());
const room = this._getRoom();
const partCreator = new PartCreator(
() => this._autocompleteRef,
query => this.setState({query}),
Expand All @@ -59,6 +60,11 @@ export default class MessageEditor extends React.Component {
};
this._editorRef = null;
this._autocompleteRef = null;
this._hasModifications = false;
}

_getRoom() {
return this.context.matrixClient.getRoom(this.props.event.getRoomId());
}

_updateEditorState = (caret) => {
Expand All @@ -74,11 +80,22 @@ export default class MessageEditor extends React.Component {
}

_onInput = (event) => {
this._hasModifications = true;
const sel = document.getSelection();
const {caret, text} = getCaretOffsetAndText(this._editorRef, sel);
this.model.update(text, event.inputType, caret);
}

_isCaretAtStart() {
const {caret} = getCaretOffsetAndText(this._editorRef, document.getSelection());
return caret.offset === 0;
}

_isCaretAtEnd() {
const {caret, text} = getCaretOffsetAndText(this._editorRef, document.getSelection());
return caret.offset === text.length;
}

_onKeyDown = (event) => {
// insert newline on Shift+Enter
if (event.shiftKey && event.key === "Enter") {
Expand Down Expand Up @@ -112,6 +129,27 @@ export default class MessageEditor extends React.Component {
event.preventDefault();
} else if (event.key === "Escape") {
this._cancelEdit();
} else if (event.key === "ArrowUp") {
if (this._hasModifications || !this._isCaretAtStart()) {
return;
}
const previousEvent = findPreviousEditableEvent(this._getRoom(), this.props.event.getId());
if (previousEvent) {
dis.dispatch({action: 'edit_event', event: previousEvent});
event.preventDefault();
}
} else if (event.key === "ArrowDown") {
if (this._hasModifications || !this._isCaretAtEnd()) {
return;
}
const nextEvent = findNextEditableEvent(this._getRoom(), this.props.event.getId());
if (nextEvent) {
dis.dispatch({action: 'edit_event', event: nextEvent});
} else {
dis.dispatch({action: 'edit_event', event: null});
dis.dispatch({action: 'focus_composer'});
}
event.preventDefault();
}
}

Expand Down
74 changes: 10 additions & 64 deletions src/components/views/rooms/MessageComposerInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ import * as HtmlUtils from '../../../HtmlUtils';
import Autocomplete from './Autocomplete';
import {Completion} from "../../../autocomplete/Autocompleter";
import Markdown from '../../../Markdown';
import ComposerHistoryManager from '../../../ComposerHistoryManager';
import MessageComposerStore from '../../../stores/MessageComposerStore';
import ContentMessages from '../../../ContentMessages';

Expand All @@ -60,6 +59,7 @@ import RoomViewStore from '../../../stores/RoomViewStore';
import ReplyThread from "../elements/ReplyThread";
import {ContentHelpers} from 'matrix-js-sdk';
import AccessibleButton from '../elements/AccessibleButton';
import { findPreviousEditableEvent } from '../../../utils/EventUtils';

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

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

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

constructor(props, context) {
super(props, context);
Expand Down Expand Up @@ -330,7 +329,6 @@ 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 @@ -1031,7 +1029,6 @@ 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 @@ -1109,11 +1106,6 @@ 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 @@ -1188,69 +1180,23 @@ export default class MessageComposerInput extends React.Component {
// and we must be at the edge of the document (up=start, down=end)
if (up) {
if (!selection.anchor.isAtStartOfNode(document)) return;
} else {
if (!selection.anchor.isAtEndOfNode(document)) return;
}

const selected = this.selectHistory(up);
if (selected) {
// We're selecting history, so prevent the key event from doing anything else
e.preventDefault();
const editEvent = findPreviousEditableEvent(this.props.room);
if (editEvent) {
// We're selecting history, so prevent the key event from doing anything else
e.preventDefault();
dis.dispatch({
action: 'edit_event',
event: editEvent,
});
}
}
} else {
this.moveAutocompleteSelection(up);
e.preventDefault();
}
};

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
35 changes: 34 additions & 1 deletion src/utils/EventUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ limitations under the License.

import { EventStatus } from 'matrix-js-sdk';
import MatrixClientPeg from '../MatrixClientPeg';

import { findLastIndex, findIndex } from "lodash";
bwindels marked this conversation as resolved.
Show resolved Hide resolved
import shouldHideEvent from "../shouldHideEvent";
/**
* Returns whether an event should allow actions like reply, reactions, edit, etc.
* which effectively checks whether it's a regular message that has been sent and that we
Expand Down Expand Up @@ -50,3 +51,35 @@ export function canEditContent(mxEvent) {
mxEvent.getOriginalContent().msgtype === "m.text" &&
mxEvent.getSender() === MatrixClientPeg.get().getUserId();
}

export function findPreviousEditableEvent(room, fromEventId = undefined) {
bwindels marked this conversation as resolved.
Show resolved Hide resolved
const liveTimeline = room.getLiveTimeline();
const events = liveTimeline.getEvents();
let startFromIdx = events.length - 1;
if (fromEventId) {
const fromEventIdx = findLastIndex(events, e => e.getId() === fromEventId);
if (fromEventIdx !== -1) {
startFromIdx = fromEventIdx - 1;
}
}
const nextEventIdx = findLastIndex(events, e => !shouldHideEvent(e) && canEditContent(e), startFromIdx);
bwindels marked this conversation as resolved.
Show resolved Hide resolved
bwindels marked this conversation as resolved.
Show resolved Hide resolved
if (nextEventIdx !== -1) {
return events[nextEventIdx];
}
}

export function findNextEditableEvent(room, fromEventId = undefined) {
const liveTimeline = room.getLiveTimeline();
const events = liveTimeline.getEvents();
let startFromIdx = 0;
if (fromEventId) {
const fromEventIdx = findIndex(events, e => e.getId() === fromEventId);
if (fromEventIdx !== -1) {
startFromIdx = fromEventIdx + 1;
}
}
const nextEventIdx = findIndex(events, e => !shouldHideEvent(e) && canEditContent(e), startFromIdx);
if (nextEventIdx !== -1) {
return events[nextEventIdx];
}
}