use EditorStateTransfer to pass on state to newly mounted editor

This commit is contained in:
Bruno Windels 2019-06-12 18:52:34 +02:00
parent e674f39e3b
commit 41e41269dc
7 changed files with 83 additions and 36 deletions

View file

@ -517,7 +517,8 @@ module.exports = React.createClass({
const DateSeparator = sdk.getComponent('messages.DateSeparator'); const DateSeparator = sdk.getComponent('messages.DateSeparator');
const ret = []; const ret = [];
const isEditing = this.props.editEvent && this.props.editEvent.getId() === mxEv.getId(); const isEditing = this.props.editState &&
this.props.editState.getEvent().getId() === mxEv.getId();
// is this a continuation of the previous message? // is this a continuation of the previous message?
let continuation = false; let continuation = false;
@ -585,7 +586,7 @@ module.exports = React.createClass({
continuation={continuation} continuation={continuation}
isRedacted={mxEv.isRedacted()} isRedacted={mxEv.isRedacted()}
replacingEventId={mxEv.replacingEventId()} replacingEventId={mxEv.replacingEventId()}
isEditing={isEditing} editState={isEditing && this.props.editState}
onHeightChanged={this._onHeightChanged} onHeightChanged={this._onHeightChanged}
readReceipts={readReceipts} readReceipts={readReceipts}
readReceiptMap={this._readReceiptMap} readReceiptMap={this._readReceiptMap}

View file

@ -35,6 +35,7 @@ const Modal = require("../../Modal");
const UserActivity = require("../../UserActivity"); const UserActivity = require("../../UserActivity");
import { KeyCode } from '../../Keyboard'; import { KeyCode } from '../../Keyboard';
import Timer from '../../utils/Timer'; import Timer from '../../utils/Timer';
import EditorStateTransfer from '../../utils/EditorStateTransfer';
const PAGINATE_SIZE = 20; const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20; const INITIAL_SIZE = 20;
@ -411,7 +412,8 @@ const TimelinePanel = React.createClass({
this.forceUpdate(); this.forceUpdate();
} }
if (payload.action === "edit_event") { if (payload.action === "edit_event") {
this.setState({editEvent: payload.event}, () => { const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
this.setState({editState}, () => {
if (payload.event && this.refs.messagePanel) { if (payload.event && this.refs.messagePanel) {
this.refs.messagePanel.scrollToEventIfNeeded( this.refs.messagePanel.scrollToEventIfNeeded(
payload.event.getId(), payload.event.getId(),
@ -1306,7 +1308,7 @@ const TimelinePanel = React.createClass({
tileShape={this.props.tileShape} tileShape={this.props.tileShape}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
getRelationsForEvent={this.getRelationsForEvent} getRelationsForEvent={this.getRelationsForEvent}
editEvent={this.state.editEvent} editState={this.state.editState}
showReactions={this.props.showReactions} showReactions={this.props.showReactions}
/> />
); );

View file

@ -28,13 +28,14 @@ import {parseEvent} from '../../../editor/deserialize';
import Autocomplete from '../rooms/Autocomplete'; import Autocomplete from '../rooms/Autocomplete';
import {PartCreator} from '../../../editor/parts'; import {PartCreator} from '../../../editor/parts';
import {renderModel} from '../../../editor/render'; import {renderModel} from '../../../editor/render';
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk'; import EditorStateTransfer from '../../../utils/EditorStateTransfer';
import {MatrixClient} from 'matrix-js-sdk';
import classNames from 'classnames'; import classNames from 'classnames';
export default class MessageEditor extends React.Component { export default class MessageEditor extends React.Component {
static propTypes = { static propTypes = {
// the message event being edited // the message event being edited
event: PropTypes.instanceOf(MatrixEvent).isRequired, editState: PropTypes.instanceOf(EditorStateTransfer).isRequired,
}; };
static contextTypes = { static contextTypes = {
@ -44,16 +45,7 @@ export default class MessageEditor extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
const room = this._getRoom(); const room = this._getRoom();
const partCreator = new PartCreator( this.model = null;
() => this._autocompleteRef,
query => this.setState({query}),
room,
);
this.model = new EditorModel(
parseEvent(this.props.event, room),
partCreator,
this._updateEditorState,
);
this.state = { this.state = {
autoComplete: null, autoComplete: null,
room, room,
@ -64,7 +56,7 @@ export default class MessageEditor extends React.Component {
} }
_getRoom() { _getRoom() {
return this.context.matrixClient.getRoom(this.props.event.getRoomId()); return this.context.matrixClient.getRoom(this.props.editState.getEvent().getRoomId());
} }
_updateEditorState = (caret) => { _updateEditorState = (caret) => {
@ -133,7 +125,7 @@ export default class MessageEditor extends React.Component {
if (this._hasModifications || !this._isCaretAtStart()) { if (this._hasModifications || !this._isCaretAtStart()) {
return; return;
} }
const previousEvent = findEditableEvent(this._getRoom(), false, this.props.event.getId()); const previousEvent = findEditableEvent(this._getRoom(), false, this.props.editState.getEvent().getId());
if (previousEvent) { if (previousEvent) {
dis.dispatch({action: 'edit_event', event: previousEvent}); dis.dispatch({action: 'edit_event', event: previousEvent});
event.preventDefault(); event.preventDefault();
@ -142,7 +134,7 @@ export default class MessageEditor extends React.Component {
if (this._hasModifications || !this._isCaretAtEnd()) { if (this._hasModifications || !this._isCaretAtEnd()) {
return; return;
} }
const nextEvent = findEditableEvent(this._getRoom(), true, this.props.event.getId()); const nextEvent = findEditableEvent(this._getRoom(), true, this.props.editState.getEvent().getId());
if (nextEvent) { if (nextEvent) {
dis.dispatch({action: 'edit_event', event: nextEvent}); dis.dispatch({action: 'edit_event', event: nextEvent});
} else { } else {
@ -178,11 +170,11 @@ export default class MessageEditor extends React.Component {
"m.new_content": newContent, "m.new_content": newContent,
"m.relates_to": { "m.relates_to": {
"rel_type": "m.replace", "rel_type": "m.replace",
"event_id": this.props.event.getId(), "event_id": this.props.editState.getEvent().getId(),
}, },
}, contentBody); }, contentBody);
const roomId = this.props.event.getRoomId(); const roomId = this.props.editState.getEvent().getRoomId();
this.context.matrixClient.sendMessage(roomId, content); this.context.matrixClient.sendMessage(roomId, content);
dis.dispatch({action: "edit_event", event: null}); dis.dispatch({action: "edit_event", event: null});
@ -197,12 +189,63 @@ export default class MessageEditor extends React.Component {
this.model.autoComplete.onComponentSelectionChange(completion); this.model.autoComplete.onComponentSelectionChange(completion);
} }
componentWillUnmount() {
const sel = document.getSelection();
const {caret} = getCaretOffsetAndText(this._editorRef, sel);
const parts = this.model.serializeParts();
this.props.editState.setEditorState(caret, parts);
}
componentDidMount() { componentDidMount() {
this.model = this._createEditorModel();
// initial render of model
this._updateEditorState(); this._updateEditorState();
setCaretPosition(this._editorRef, this.model, this.model.getPositionAtEnd()); // initial caret position
this._initializeCaret();
this._editorRef.focus(); this._editorRef.focus();
} }
_createEditorModel() {
const {editState} = this.props;
const room = this._getRoom();
const partCreator = new PartCreator(
() => this._autocompleteRef,
query => this.setState({query}),
room,
this.context.matrixClient,
);
let parts;
if (editState.hasEditorState()) {
// if restoring state from a previous editor,
// restore serialized parts from the state
parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p));
} else {
// otherwise, parse the body of the event
parts = parseEvent(editState.getEvent(), room, this.context.matrixClient);
}
return new EditorModel(
parts,
partCreator,
this._updateEditorState,
);
}
_initializeCaret() {
const {editState} = this.props;
let caretPosition;
if (editState.hasEditorState()) {
// if restoring state from a previous editor,
// restore caret position from the state
const caret = editState.getCaret();
caretPosition = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
} else {
// otherwise, set it at the end
caretPosition = this.model.getPositionAtEnd();
}
setCaretPosition(this._editorRef, this.model, caretPosition);
}
render() { render() {
let autoComplete; let autoComplete;
if (this.state.autoComplete) { if (this.state.autoComplete) {

View file

@ -90,7 +90,7 @@ module.exports = React.createClass({
tileShape={this.props.tileShape} tileShape={this.props.tileShape}
maxImageHeight={this.props.maxImageHeight} maxImageHeight={this.props.maxImageHeight}
replacingEventId={this.props.replacingEventId} replacingEventId={this.props.replacingEventId}
isEditing={this.props.isEditing} editState={this.props.editState}
onHeightChanged={this.props.onHeightChanged} />; onHeightChanged={this.props.onHeightChanged} />;
}, },
}); });

View file

@ -90,7 +90,7 @@ module.exports = React.createClass({
componentDidMount: function() { componentDidMount: function() {
this._unmounted = false; this._unmounted = false;
if (!this.props.isEditing) { if (!this.props.editState) {
this._applyFormatting(); this._applyFormatting();
} }
}, },
@ -131,8 +131,8 @@ module.exports = React.createClass({
}, },
componentDidUpdate: function(prevProps) { componentDidUpdate: function(prevProps) {
if (!this.props.isEditing) { if (!this.props.editState) {
const stoppedEditing = prevProps.isEditing && !this.props.isEditing; const stoppedEditing = prevProps.editState && !this.props.editState;
const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId; const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId;
if (messageWasEdited || stoppedEditing) { if (messageWasEdited || stoppedEditing) {
this._applyFormatting(); this._applyFormatting();
@ -153,7 +153,7 @@ module.exports = React.createClass({
nextProps.replacingEventId !== this.props.replacingEventId || nextProps.replacingEventId !== this.props.replacingEventId ||
nextProps.highlightLink !== this.props.highlightLink || nextProps.highlightLink !== this.props.highlightLink ||
nextProps.showUrlPreview !== this.props.showUrlPreview || nextProps.showUrlPreview !== this.props.showUrlPreview ||
nextProps.isEditing !== this.props.isEditing || nextProps.editState !== this.props.editState ||
nextState.links !== this.state.links || nextState.links !== this.state.links ||
nextState.editedMarkerHovered !== this.state.editedMarkerHovered || nextState.editedMarkerHovered !== this.state.editedMarkerHovered ||
nextState.widgetHidden !== this.state.widgetHidden); nextState.widgetHidden !== this.state.widgetHidden);
@ -469,9 +469,9 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
if (this.props.isEditing) { if (this.props.editState) {
const MessageEditor = sdk.getComponent('elements.MessageEditor'); const MessageEditor = sdk.getComponent('elements.MessageEditor');
return <MessageEditor event={this.props.mxEvent} className="mx_EventTile_content" />; return <MessageEditor editState={this.props.editState} className="mx_EventTile_content" />;
} }
const mxEvent = this.props.mxEvent; const mxEvent = this.props.mxEvent;
const content = mxEvent.getContent(); const content = mxEvent.getContent();

View file

@ -552,13 +552,14 @@ module.exports = withMatrixClient(React.createClass({
const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted; const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted;
const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure(); const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure();
const isEditing = !!this.props.editState;
const classes = classNames({ const classes = classNames({
mx_EventTile: true, mx_EventTile: true,
mx_EventTile_isEditing: this.props.isEditing, mx_EventTile_isEditing: isEditing,
mx_EventTile_info: isInfoMessage, mx_EventTile_info: isInfoMessage,
mx_EventTile_12hr: this.props.isTwelveHour, mx_EventTile_12hr: this.props.isTwelveHour,
mx_EventTile_encrypting: this.props.eventSendStatus === 'encrypting', mx_EventTile_encrypting: this.props.eventSendStatus === 'encrypting',
mx_EventTile_sending: isSending, mx_EventTile_sending: !isEditing && isSending,
mx_EventTile_notSent: this.props.eventSendStatus === 'not_sent', mx_EventTile_notSent: this.props.eventSendStatus === 'not_sent',
mx_EventTile_highlight: this.props.tileShape === 'notif' ? false : this.shouldHighlight(), mx_EventTile_highlight: this.props.tileShape === 'notif' ? false : this.shouldHighlight(),
mx_EventTile_selected: this.props.isSelectedEvent, mx_EventTile_selected: this.props.isSelectedEvent,
@ -632,7 +633,7 @@ module.exports = withMatrixClient(React.createClass({
} }
const MessageActionBar = sdk.getComponent('messages.MessageActionBar'); const MessageActionBar = sdk.getComponent('messages.MessageActionBar');
const actionBar = !this.props.isEditing ? <MessageActionBar const actionBar = !isEditing ? <MessageActionBar
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
reactions={this.state.reactions} reactions={this.state.reactions}
permalinkCreator={this.props.permalinkCreator} permalinkCreator={this.props.permalinkCreator}
@ -794,7 +795,7 @@ module.exports = withMatrixClient(React.createClass({
<EventTileType ref="tile" <EventTileType ref="tile"
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
replacingEventId={this.props.replacingEventId} replacingEventId={this.props.replacingEventId}
isEditing={this.props.isEditing} editState={this.props.editState}
highlights={this.props.highlights} highlights={this.props.highlights}
highlightLink={this.props.highlightLink} highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview} showUrlPreview={this.props.showUrlPreview}

View file

@ -88,7 +88,7 @@ export default class EditorModel {
update(newValue, inputType, caret) { update(newValue, inputType, caret) {
const diff = this._diff(newValue, inputType, caret); const diff = this._diff(newValue, inputType, caret);
const position = this._positionForOffset(diff.at, caret.atNodeEnd); const position = this.positionForOffset(diff.at, caret.atNodeEnd);
let removedOffsetDecrease = 0; let removedOffsetDecrease = 0;
if (diff.removed) { if (diff.removed) {
removedOffsetDecrease = this._removeText(position, diff.removed.length); removedOffsetDecrease = this._removeText(position, diff.removed.length);
@ -99,7 +99,7 @@ export default class EditorModel {
} }
this._mergeAdjacentParts(); this._mergeAdjacentParts();
const caretOffset = diff.at - removedOffsetDecrease + addedLen; const caretOffset = diff.at - removedOffsetDecrease + addedLen;
let newPosition = this._positionForOffset(caretOffset, true); let newPosition = this.positionForOffset(caretOffset, true);
newPosition = newPosition.skipUneditableParts(this._parts); newPosition = newPosition.skipUneditableParts(this._parts);
this._setActivePart(newPosition); this._setActivePart(newPosition);
this._updateCallback(newPosition); this._updateCallback(newPosition);
@ -248,7 +248,7 @@ export default class EditorModel {
return addLen; return addLen;
} }
_positionForOffset(totalOffset, atPartEnd) { positionForOffset(totalOffset, atPartEnd) {
let currentOffset = 0; let currentOffset = 0;
const index = this._parts.findIndex(part => { const index = this._parts.findIndex(part => {
const partLen = part.text.length; const partLen = part.text.length;