diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index 698768067a..52fd6d9be4 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -517,7 +517,8 @@ module.exports = React.createClass({
const DateSeparator = sdk.getComponent('messages.DateSeparator');
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?
let continuation = false;
@@ -585,7 +586,7 @@ module.exports = React.createClass({
continuation={continuation}
isRedacted={mxEv.isRedacted()}
replacingEventId={mxEv.replacingEventId()}
- isEditing={isEditing}
+ editState={isEditing && this.props.editState}
onHeightChanged={this._onHeightChanged}
readReceipts={readReceipts}
readReceiptMap={this._readReceiptMap}
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index 220c56d754..9c48b8ede1 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -35,6 +35,7 @@ const Modal = require("../../Modal");
const UserActivity = require("../../UserActivity");
import { KeyCode } from '../../Keyboard';
import Timer from '../../utils/Timer';
+import EditorStateTransfer from '../../utils/EditorStateTransfer';
const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20;
@@ -411,7 +412,8 @@ const TimelinePanel = React.createClass({
this.forceUpdate();
}
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) {
this.refs.messagePanel.scrollToEventIfNeeded(
payload.event.getId(),
@@ -1306,7 +1308,7 @@ const TimelinePanel = React.createClass({
tileShape={this.props.tileShape}
resizeNotifier={this.props.resizeNotifier}
getRelationsForEvent={this.getRelationsForEvent}
- editEvent={this.state.editEvent}
+ editState={this.state.editState}
showReactions={this.props.showReactions}
/>
);
diff --git a/src/components/views/elements/MessageEditor.js b/src/components/views/elements/MessageEditor.js
index ed86bcb0a3..0aff6781ee 100644
--- a/src/components/views/elements/MessageEditor.js
+++ b/src/components/views/elements/MessageEditor.js
@@ -28,13 +28,14 @@ import {parseEvent} from '../../../editor/deserialize';
import Autocomplete from '../rooms/Autocomplete';
import {PartCreator} from '../../../editor/parts';
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';
export default class MessageEditor extends React.Component {
static propTypes = {
// the message event being edited
- event: PropTypes.instanceOf(MatrixEvent).isRequired,
+ editState: PropTypes.instanceOf(EditorStateTransfer).isRequired,
};
static contextTypes = {
@@ -44,16 +45,7 @@ export default class MessageEditor extends React.Component {
constructor(props, context) {
super(props, context);
const room = this._getRoom();
- const partCreator = new PartCreator(
- () => this._autocompleteRef,
- query => this.setState({query}),
- room,
- );
- this.model = new EditorModel(
- parseEvent(this.props.event, room),
- partCreator,
- this._updateEditorState,
- );
+ this.model = null;
this.state = {
autoComplete: null,
room,
@@ -64,7 +56,7 @@ export default class MessageEditor extends React.Component {
}
_getRoom() {
- return this.context.matrixClient.getRoom(this.props.event.getRoomId());
+ return this.context.matrixClient.getRoom(this.props.editState.getEvent().getRoomId());
}
_updateEditorState = (caret) => {
@@ -133,7 +125,7 @@ export default class MessageEditor extends React.Component {
if (this._hasModifications || !this._isCaretAtStart()) {
return;
}
- const previousEvent = findEditableEvent(this._getRoom(), false, this.props.event.getId());
+ const previousEvent = findEditableEvent(this._getRoom(), false, this.props.editState.getEvent().getId());
if (previousEvent) {
dis.dispatch({action: 'edit_event', event: previousEvent});
event.preventDefault();
@@ -142,7 +134,7 @@ export default class MessageEditor extends React.Component {
if (this._hasModifications || !this._isCaretAtEnd()) {
return;
}
- const nextEvent = findEditableEvent(this._getRoom(), true, this.props.event.getId());
+ const nextEvent = findEditableEvent(this._getRoom(), true, this.props.editState.getEvent().getId());
if (nextEvent) {
dis.dispatch({action: 'edit_event', event: nextEvent});
} else {
@@ -178,11 +170,11 @@ export default class MessageEditor extends React.Component {
"m.new_content": newContent,
"m.relates_to": {
"rel_type": "m.replace",
- "event_id": this.props.event.getId(),
+ "event_id": this.props.editState.getEvent().getId(),
},
}, contentBody);
- const roomId = this.props.event.getRoomId();
+ const roomId = this.props.editState.getEvent().getRoomId();
this.context.matrixClient.sendMessage(roomId, content);
dis.dispatch({action: "edit_event", event: null});
@@ -197,12 +189,63 @@ export default class MessageEditor extends React.Component {
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() {
+ this.model = this._createEditorModel();
+ // initial render of model
this._updateEditorState();
- setCaretPosition(this._editorRef, this.model, this.model.getPositionAtEnd());
+ // initial caret position
+ this._initializeCaret();
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() {
let autoComplete;
if (this.state.autoComplete) {
diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js
index 8c90ec5a46..6d7aada542 100644
--- a/src/components/views/messages/MessageEvent.js
+++ b/src/components/views/messages/MessageEvent.js
@@ -90,7 +90,7 @@ module.exports = React.createClass({
tileShape={this.props.tileShape}
maxImageHeight={this.props.maxImageHeight}
replacingEventId={this.props.replacingEventId}
- isEditing={this.props.isEditing}
+ editState={this.props.editState}
onHeightChanged={this.props.onHeightChanged} />;
},
});
diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index 1fc16d6a53..6f480b8d3c 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -90,7 +90,7 @@ module.exports = React.createClass({
componentDidMount: function() {
this._unmounted = false;
- if (!this.props.isEditing) {
+ if (!this.props.editState) {
this._applyFormatting();
}
},
@@ -131,8 +131,8 @@ module.exports = React.createClass({
},
componentDidUpdate: function(prevProps) {
- if (!this.props.isEditing) {
- const stoppedEditing = prevProps.isEditing && !this.props.isEditing;
+ if (!this.props.editState) {
+ const stoppedEditing = prevProps.editState && !this.props.editState;
const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId;
if (messageWasEdited || stoppedEditing) {
this._applyFormatting();
@@ -153,7 +153,7 @@ module.exports = React.createClass({
nextProps.replacingEventId !== this.props.replacingEventId ||
nextProps.highlightLink !== this.props.highlightLink ||
nextProps.showUrlPreview !== this.props.showUrlPreview ||
- nextProps.isEditing !== this.props.isEditing ||
+ nextProps.editState !== this.props.editState ||
nextState.links !== this.state.links ||
nextState.editedMarkerHovered !== this.state.editedMarkerHovered ||
nextState.widgetHidden !== this.state.widgetHidden);
@@ -469,9 +469,9 @@ module.exports = React.createClass({
},
render: function() {
- if (this.props.isEditing) {
+ if (this.props.editState) {
const MessageEditor = sdk.getComponent('elements.MessageEditor');
- return ;
+ return ;
}
const mxEvent = this.props.mxEvent;
const content = mxEvent.getContent();
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index 850f496e24..9837b4a029 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -552,13 +552,14 @@ module.exports = withMatrixClient(React.createClass({
const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted;
const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure();
+ const isEditing = !!this.props.editState;
const classes = classNames({
mx_EventTile: true,
- mx_EventTile_isEditing: this.props.isEditing,
+ mx_EventTile_isEditing: isEditing,
mx_EventTile_info: isInfoMessage,
mx_EventTile_12hr: this.props.isTwelveHour,
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_highlight: this.props.tileShape === 'notif' ? false : this.shouldHighlight(),
mx_EventTile_selected: this.props.isSelectedEvent,
@@ -632,7 +633,7 @@ module.exports = withMatrixClient(React.createClass({
}
const MessageActionBar = sdk.getComponent('messages.MessageActionBar');
- const actionBar = !this.props.isEditing ? {return {type, text};});
+ return this._parts.map(p => p.serialize());
}
_diff(newValue, inputType, caret) {
@@ -88,7 +88,7 @@ export default class EditorModel {
update(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;
if (diff.removed) {
removedOffsetDecrease = this._removeText(position, diff.removed.length);
@@ -99,7 +99,7 @@ export default class EditorModel {
}
this._mergeAdjacentParts();
const caretOffset = diff.at - removedOffsetDecrease + addedLen;
- let newPosition = this._positionForOffset(caretOffset, true);
+ let newPosition = this.positionForOffset(caretOffset, true);
newPosition = newPosition.skipUneditableParts(this._parts);
this._setActivePart(newPosition);
this._updateCallback(newPosition);
@@ -248,7 +248,7 @@ export default class EditorModel {
return addLen;
}
- _positionForOffset(totalOffset, atPartEnd) {
+ positionForOffset(totalOffset, atPartEnd) {
let currentOffset = 0;
const index = this._parts.findIndex(part => {
const partLen = part.text.length;
diff --git a/src/editor/parts.js b/src/editor/parts.js
index be3080db12..a122c7ab7a 100644
--- a/src/editor/parts.js
+++ b/src/editor/parts.js
@@ -17,7 +17,6 @@ limitations under the License.
import AutocompleteWrapperModel from "./autocomplete";
import Avatar from "../Avatar";
-import MatrixClientPeg from "../MatrixClientPeg";
class BasePart {
constructor(text = "") {
@@ -102,6 +101,10 @@ class BasePart {
toString() {
return `${this.type}(${this.text})`;
}
+
+ serialize() {
+ return {type: this.type, text: this.text};
+ }
}
export class PlainPart extends BasePart {
@@ -233,13 +236,12 @@ export class NewlinePart extends BasePart {
}
export class RoomPillPart extends PillPart {
- constructor(displayAlias) {
+ constructor(displayAlias, client) {
super(displayAlias, displayAlias);
- this._room = this._findRoomByAlias(displayAlias);
+ this._room = this._findRoomByAlias(displayAlias, client);
}
- _findRoomByAlias(alias) {
- const client = MatrixClientPeg.get();
+ _findRoomByAlias(alias, client) {
if (alias[0] === '#') {
return client.getRooms().find((r) => {
return r.getAliases().includes(alias);
@@ -300,6 +302,12 @@ export class UserPillPart extends PillPart {
get className() {
return "mx_UserPill mx_Pill";
}
+
+ serialize() {
+ const obj = super.serialize();
+ obj.userId = this.resourceId;
+ return obj;
+ }
}
@@ -335,13 +343,16 @@ export class PillCandidatePart extends PlainPart {
}
export class PartCreator {
- constructor(getAutocompleterComponent, updateQuery, room) {
+ constructor(getAutocompleterComponent, updateQuery, room, client) {
+ this._room = room;
+ this._client = client;
this._autoCompleteCreator = (updateCallback) => {
return new AutocompleteWrapperModel(
updateCallback,
getAutocompleterComponent,
updateQuery,
room,
+ client,
);
};
}
@@ -362,5 +373,22 @@ export class PartCreator {
createDefaultPart(text) {
return new PlainPart(text);
}
+
+ deserializePart(part) {
+ switch (part.type) {
+ case "plain":
+ return new PlainPart(part.text);
+ case "newline":
+ return new NewlinePart(part.text);
+ case "pill-candidate":
+ return new PillCandidatePart(part.text, this._autoCompleteCreator);
+ case "room-pill":
+ return new RoomPillPart(part.text, this._client);
+ case "user-pill": {
+ const member = this._room.getMember(part.userId);
+ return new UserPillPart(part.userId, part.text, member);
+ }
+ }
+ }
}
diff --git a/src/utils/EditorStateTransfer.js b/src/utils/EditorStateTransfer.js
new file mode 100644
index 0000000000..c7782a9ea8
--- /dev/null
+++ b/src/utils/EditorStateTransfer.js
@@ -0,0 +1,49 @@
+/*
+Copyright 2019 The Matrix.org Foundation C.I.C.
+
+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.
+*/
+
+/**
+ * Used while editing, to pass the event, and to preserve editor state
+ * from one editor instance to another when remounting the editor
+ * upon receiving the remote echo for an unsent event.
+ */
+export default class EditorStateTransfer {
+ constructor(event) {
+ this._event = event;
+ this._serializedParts = null;
+ this.caret = null;
+ }
+
+ setEditorState(caret, serializedParts) {
+ this._caret = caret;
+ this._serializedParts = serializedParts;
+ }
+
+ hasEditorState() {
+ return !!this._serializedParts;
+ }
+
+ getSerializedParts() {
+ return this._serializedParts;
+ }
+
+ getCaret() {
+ return this._caret;
+ }
+
+ getEvent() {
+ return this._event;
+ }
+}
diff --git a/src/utils/EventUtils.js b/src/utils/EventUtils.js
index ff20a68e3c..219b53bc5e 100644
--- a/src/utils/EventUtils.js
+++ b/src/utils/EventUtils.js
@@ -46,7 +46,8 @@ export function isContentActionable(mxEvent) {
}
export function canEditContent(mxEvent) {
- return isContentActionable(mxEvent) &&
+ return mxEvent.status !== EventStatus.CANCELLED &&
+ mxEvent.getType() === 'm.room.message' &&
mxEvent.getOriginalContent().msgtype === "m.text" &&
mxEvent.getSender() === MatrixClientPeg.get().getUserId();
}
@@ -64,7 +65,7 @@ export function canEditOwnEvent(mxEvent) {
const MAX_JUMP_DISTANCE = 100;
export function findEditableEvent(room, isForward, fromEventId = undefined) {
const liveTimeline = room.getLiveTimeline();
- const events = liveTimeline.getEvents();
+ const events = liveTimeline.getEvents().concat(room.getPendingEvents());
const maxIdx = events.length - 1;
const inc = isForward ? 1 : -1;
const beginIdx = isForward ? 0 : maxIdx;
diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js
index 88d1c804ca..2d1fb29bd9 100644
--- a/test/components/views/dialogs/InteractiveAuthDialog-test.js
+++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js
@@ -103,12 +103,6 @@ describe('InteractiveAuthDialog', function() {
password: "s3kr3t",
user: "@user:id",
})).toBe(true);
-
- // there should now be a spinner
- ReactTestUtils.findRenderedComponentWithType(
- dlg, sdk.getComponent('elements.Spinner'),
- );
-
// let the request complete
return Promise.delay(1);
}).then(() => {