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

Commit cd80361

Browse files
authored
Merge pull request #3192 from matrix-org/bwindels/preserve-reply-fallback-on-edit
Preserve reply fallback on edit
2 parents 0347c42 + f6e71b2 commit cd80361

File tree

3 files changed

+95
-49
lines changed

3 files changed

+95
-49
lines changed

src/components/views/elements/MessageEditor.js

Lines changed: 92 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,80 @@ import {MatrixClient} from 'matrix-js-sdk';
3333
import classNames from 'classnames';
3434
import {EventStatus} from 'matrix-js-sdk';
3535

36+
function _isReply(mxEvent) {
37+
const relatesTo = mxEvent.getContent()["m.relates_to"];
38+
const isReply = !!(relatesTo && relatesTo["m.in_reply_to"]);
39+
return isReply;
40+
}
41+
42+
function getHtmlReplyFallback(mxEvent) {
43+
const html = mxEvent.getContent().formatted_body;
44+
if (!html) {
45+
return "";
46+
}
47+
const rootNode = new DOMParser().parseFromString(html, "text/html").body;
48+
const mxReply = rootNode.querySelector("mx-reply");
49+
return (mxReply && mxReply.outerHTML) || "";
50+
}
51+
52+
function getTextReplyFallback(mxEvent) {
53+
const body = mxEvent.getContent().body;
54+
const lines = body.split("\n").map(l => l.trim());
55+
if (lines.length > 2 && lines[0].startsWith("> ") && lines[1].length === 0) {
56+
return `${lines[0]}\n\n`;
57+
}
58+
return "";
59+
}
60+
61+
function _isEmote(model) {
62+
const firstPart = model.parts[0];
63+
return firstPart && firstPart.type === "plain" && firstPart.text.startsWith("/me ");
64+
}
65+
66+
function createEditContent(model, editedEvent) {
67+
const isEmote = _isEmote(model);
68+
if (isEmote) {
69+
// trim "/me "
70+
model = model.clone();
71+
model.removeText({index: 0, offset: 0}, 4);
72+
}
73+
const isReply = _isReply(editedEvent);
74+
let plainPrefix = "";
75+
let htmlPrefix = "";
76+
77+
if (isReply) {
78+
plainPrefix = getTextReplyFallback(editedEvent);
79+
htmlPrefix = getHtmlReplyFallback(editedEvent);
80+
}
81+
82+
const body = textSerialize(model);
83+
84+
const newContent = {
85+
"msgtype": isEmote ? "m.emote" : "m.text",
86+
"body": plainPrefix + body,
87+
};
88+
const contentBody = {
89+
msgtype: newContent.msgtype,
90+
body: `${plainPrefix} * ${body}`,
91+
};
92+
93+
const formattedBody = htmlSerializeIfNeeded(model, {forceHTML: isReply});
94+
if (formattedBody) {
95+
newContent.format = "org.matrix.custom.html";
96+
newContent.formatted_body = htmlPrefix + formattedBody;
97+
contentBody.format = newContent.format;
98+
contentBody.formatted_body = `${htmlPrefix} * ${formattedBody}`;
99+
}
100+
101+
return Object.assign({
102+
"m.new_content": newContent,
103+
"m.relates_to": {
104+
"rel_type": "m.replace",
105+
"event_id": editedEvent.getId(),
106+
},
107+
}, contentBody);
108+
}
109+
36110
export default class MessageEditor extends React.Component {
37111
static propTypes = {
38112
// the message event being edited
@@ -53,7 +127,7 @@ export default class MessageEditor extends React.Component {
53127
};
54128
this._editorRef = null;
55129
this._autocompleteRef = null;
56-
this._hasModifications = false;
130+
this._modifiedFlag = false;
57131
}
58132

59133
_getRoom() {
@@ -73,7 +147,7 @@ export default class MessageEditor extends React.Component {
73147
}
74148

75149
_onInput = (event) => {
76-
this._hasModifications = true;
150+
this._modifiedFlag = true;
77151
const sel = document.getSelection();
78152
const {caret, text} = getCaretOffsetAndText(this._editorRef, sel);
79153
this.model.update(text, event.inputType, caret);
@@ -131,7 +205,7 @@ export default class MessageEditor extends React.Component {
131205
} else if (event.key === "Escape") {
132206
this._cancelEdit();
133207
} else if (event.key === "ArrowUp") {
134-
if (this._hasModifications || !this._isCaretAtStart()) {
208+
if (this._modifiedFlag || !this._isCaretAtStart()) {
135209
return;
136210
}
137211
const previousEvent = findEditableEvent(this._getRoom(), false, this.props.editState.getEvent().getId());
@@ -140,7 +214,7 @@ export default class MessageEditor extends React.Component {
140214
event.preventDefault();
141215
}
142216
} else if (event.key === "ArrowDown") {
143-
if (this._hasModifications || !this._isCaretAtEnd()) {
217+
if (this._modifiedFlag || !this._isCaretAtEnd()) {
144218
return;
145219
}
146220
const nextEvent = findEditableEvent(this._getRoom(), true, this.props.editState.getEvent().getId());
@@ -159,56 +233,28 @@ export default class MessageEditor extends React.Component {
159233
dis.dispatch({action: 'focus_composer'});
160234
}
161235

162-
_isEmote() {
163-
const firstPart = this.model.parts[0];
164-
return firstPart && firstPart.type === "plain" && firstPart.text.startsWith("/me ");
165-
}
166-
167-
_sendEdit = () => {
168-
const isEmote = this._isEmote();
169-
let model = this.model;
170-
if (isEmote) {
171-
// trim "/me "
172-
model = model.clone();
173-
model.removeText({index: 0, offset: 0}, 4);
174-
}
175-
const newContent = {
176-
"msgtype": isEmote ? "m.emote" : "m.text",
177-
"body": textSerialize(model),
178-
};
179-
const contentBody = {
180-
msgtype: newContent.msgtype,
181-
body: ` * ${newContent.body}`,
182-
};
183-
const formattedBody = htmlSerializeIfNeeded(model);
184-
if (formattedBody) {
185-
newContent.format = "org.matrix.custom.html";
186-
newContent.formatted_body = formattedBody;
187-
contentBody.format = newContent.format;
188-
contentBody.formatted_body = ` * ${newContent.formatted_body}`;
189-
}
190-
236+
_hasModifications(newContent) {
191237
// if nothing has changed then bail
192238
const oldContent = this.props.editState.getEvent().getContent();
193-
if (!this._hasModifications ||
239+
if (!this._modifiedFlag ||
194240
(oldContent["msgtype"] === newContent["msgtype"] && oldContent["body"] === newContent["body"] &&
195241
oldContent["format"] === newContent["format"] &&
196242
oldContent["formatted_body"] === newContent["formatted_body"])) {
197-
this._cancelEdit();
198-
return;
243+
return false;
199244
}
245+
return true;
246+
}
200247

201-
const content = Object.assign({
202-
"m.new_content": newContent,
203-
"m.relates_to": {
204-
"rel_type": "m.replace",
205-
"event_id": this.props.editState.getEvent().getId(),
206-
},
207-
}, contentBody);
208-
209-
const roomId = this.props.editState.getEvent().getRoomId();
248+
_sendEdit = () => {
249+
const editedEvent = this.props.editState.getEvent();
250+
const editContent = createEditContent(this.model, editedEvent);
251+
const newContent = editContent["m.new_content"];
252+
if (!this._hasModifications(newContent)) {
253+
return;
254+
}
255+
const roomId = editedEvent.getRoomId();
210256
this._cancelPreviousPendingEdit();
211-
this.context.matrixClient.sendMessage(roomId, content);
257+
this.context.matrixClient.sendMessage(roomId, editContent);
212258

213259
dis.dispatch({action: "edit_event", event: null});
214260
dis.dispatch({action: 'focus_composer'});

src/components/views/messages/EditHistoryMessage.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export default class EditHistoryMessage extends React.PureComponent {
119119
const {mxEvent} = this.props;
120120
const originalContent = mxEvent.getOriginalContent();
121121
const content = originalContent["m.new_content"] || originalContent;
122-
const contentElements = HtmlUtils.bodyToHtml(content);
122+
const contentElements = HtmlUtils.bodyToHtml(content, null, {stripReplyFallback: true});
123123
let contentContainer;
124124
if (mxEvent.isRedacted()) {
125125
const UnknownBody = sdk.getComponent('messages.UnknownBody');

src/editor/serialize.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ export function mdSerialize(model) {
3333
}, "");
3434
}
3535

36-
export function htmlSerializeIfNeeded(model) {
36+
export function htmlSerializeIfNeeded(model, {forceHTML = false}) {
3737
const md = mdSerialize(model);
3838
const parser = new Markdown(md);
39-
if (!parser.isPlainText()) {
39+
if (!parser.isPlainText() || forceHTML) {
4040
return parser.toHTML();
4141
}
4242
}

0 commit comments

Comments
 (0)