Track replyToEvent along with CIDER state & history

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2020-10-06 14:47:53 +01:00
parent 45fa647655
commit 120f269190
4 changed files with 72 additions and 34 deletions

View file

@ -16,11 +16,18 @@ limitations under the License.
*/ */
import {clamp} from "lodash"; import {clamp} from "lodash";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {SerializedPart} from "./editor/parts"; import {SerializedPart} from "./editor/parts";
import EditorModel from "./editor/model"; import EditorModel from "./editor/model";
interface IHistoryItem {
parts: SerializedPart[];
replyEventId?: string;
}
export default class SendHistoryManager { export default class SendHistoryManager {
history: Array<SerializedPart[]> = []; history: Array<IHistoryItem> = [];
prefix: string; prefix: string;
lastIndex = 0; // used for indexing the storage lastIndex = 0; // used for indexing the storage
currentIndex = 0; // used for indexing the loaded validated history Array currentIndex = 0; // used for indexing the loaded validated history Array
@ -34,8 +41,7 @@ export default class SendHistoryManager {
while (itemJSON = sessionStorage.getItem(`${this.prefix}[${index}]`)) { while (itemJSON = sessionStorage.getItem(`${this.prefix}[${index}]`)) {
try { try {
const serializedParts = JSON.parse(itemJSON); this.history.push(SendHistoryManager.parseItem(JSON.parse(itemJSON)));
this.history.push(serializedParts);
} catch (e) { } catch (e) {
console.warn("Throwing away unserialisable history", e); console.warn("Throwing away unserialisable history", e);
break; break;
@ -47,15 +53,32 @@ export default class SendHistoryManager {
this.currentIndex = this.lastIndex + 1; this.currentIndex = this.lastIndex + 1;
} }
save(editorModel: EditorModel) { static createItem(model: EditorModel, replyEvent?: MatrixEvent): IHistoryItem {
const serializedParts = editorModel.serializeParts(); return {
this.history.push(serializedParts); parts: model.serializeParts(),
this.currentIndex = this.history.length; replyEventId: replyEvent ? replyEvent.getId() : undefined,
this.lastIndex += 1; };
sessionStorage.setItem(`${this.prefix}[${this.lastIndex}]`, JSON.stringify(serializedParts));
} }
getItem(offset: number): SerializedPart[] { static parseItem(item: IHistoryItem | SerializedPart[]): IHistoryItem {
if (Array.isArray(item)) {
// XXX: migrate from old format already in Storage
return {
parts: item,
};
}
return item;
}
save(editorModel: EditorModel, replyEvent?: MatrixEvent) {
const item = SendHistoryManager.createItem(editorModel, replyEvent);
this.history.push(item);
this.currentIndex = this.history.length;
this.lastIndex += 1;
sessionStorage.setItem(`${this.prefix}[${this.lastIndex}]`, JSON.stringify(item));
}
getItem(offset: number): IHistoryItem {
this.currentIndex = clamp(this.currentIndex + offset, 0, this.history.length - 1); this.currentIndex = clamp(this.currentIndex + offset, 0, this.history.length - 1);
return this.history[this.currentIndex]; return this.history[this.currentIndex];
} }

View file

@ -92,7 +92,7 @@ interface IProps {
label?: string; label?: string;
initialCaret?: DocumentOffset; initialCaret?: DocumentOffset;
onChange(); onChange?();
onPaste?(event: ClipboardEvent<HTMLDivElement>, model: EditorModel): boolean; onPaste?(event: ClipboardEvent<HTMLDivElement>, model: EditorModel): boolean;
} }

View file

@ -257,7 +257,7 @@ export default class MessageComposer extends React.Component {
this._dispatcherRef = null; this._dispatcherRef = null;
this.state = { this.state = {
isQuoting: Boolean(RoomViewStore.getQuotingEvent()), replyToEvent: RoomViewStore.getQuotingEvent(),
tombstone: this._getRoomTombstone(), tombstone: this._getRoomTombstone(),
canSendMessages: this.props.room.maySendMessage(), canSendMessages: this.props.room.maySendMessage(),
showCallButtons: SettingsStore.getValue("showCallButtonsInComposer"), showCallButtons: SettingsStore.getValue("showCallButtonsInComposer"),
@ -337,9 +337,9 @@ export default class MessageComposer extends React.Component {
} }
_onRoomViewStoreUpdate() { _onRoomViewStoreUpdate() {
const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); const replyToEvent = RoomViewStore.getQuotingEvent();
if (this.state.isQuoting === isQuoting) return; if (this.state.replyToEvent === replyToEvent) return;
this.setState({ isQuoting }); this.setState({ replyToEvent });
} }
onInputStateChanged(inputState) { onInputStateChanged(inputState) {
@ -378,7 +378,7 @@ export default class MessageComposer extends React.Component {
} }
renderPlaceholderText() { renderPlaceholderText() {
if (this.state.isQuoting) { if (this.state.replyToEvent) {
if (this.props.e2eStatus) { if (this.props.e2eStatus) {
return _t('Send an encrypted reply…'); return _t('Send an encrypted reply…');
} else { } else {
@ -423,7 +423,9 @@ export default class MessageComposer extends React.Component {
room={this.props.room} room={this.props.room}
placeholder={this.renderPlaceholderText()} placeholder={this.renderPlaceholderText()}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
permalinkCreator={this.props.permalinkCreator} />, permalinkCreator={this.props.permalinkCreator}
replyToEvent={this.state.replyToEvent}
/>,
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />, <UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />, <EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
); );

View file

@ -29,7 +29,6 @@ import {
} from '../../../editor/serialize'; } from '../../../editor/serialize';
import {CommandPartCreator} from '../../../editor/parts'; import {CommandPartCreator} from '../../../editor/parts';
import BasicMessageComposer from "./BasicMessageComposer"; import BasicMessageComposer from "./BasicMessageComposer";
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 {findEditableEvent} from '../../../utils/EventUtils';
@ -61,7 +60,7 @@ function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
} }
// exported for tests // exported for tests
export function createMessageContent(model, permalinkCreator) { export function createMessageContent(model, permalinkCreator, replyToEvent) {
const isEmote = containsEmote(model); const isEmote = containsEmote(model);
if (isEmote) { if (isEmote) {
model = stripEmoteCommand(model); model = stripEmoteCommand(model);
@ -70,21 +69,20 @@ export function createMessageContent(model, permalinkCreator) {
model = stripPrefix(model, "/"); model = stripPrefix(model, "/");
} }
model = unescapeMessage(model); model = unescapeMessage(model);
const repliedToEvent = RoomViewStore.getQuotingEvent();
const body = textSerialize(model); const body = textSerialize(model);
const content = { const content = {
msgtype: isEmote ? "m.emote" : "m.text", msgtype: isEmote ? "m.emote" : "m.text",
body: body, body: body,
}; };
const formattedBody = htmlSerializeIfNeeded(model, {forceHTML: !!repliedToEvent}); const formattedBody = htmlSerializeIfNeeded(model, {forceHTML: !!replyToEvent});
if (formattedBody) { if (formattedBody) {
content.format = "org.matrix.custom.html"; content.format = "org.matrix.custom.html";
content.formatted_body = formattedBody; content.formatted_body = formattedBody;
} }
if (repliedToEvent) { if (replyToEvent) {
addReplyToMessageContent(content, repliedToEvent, permalinkCreator); addReplyToMessageContent(content, replyToEvent, permalinkCreator);
} }
return content; return content;
@ -95,6 +93,7 @@ export default class SendMessageComposer extends React.Component {
room: PropTypes.object.isRequired, room: PropTypes.object.isRequired,
placeholder: PropTypes.string, placeholder: PropTypes.string,
permalinkCreator: PropTypes.object.isRequired, permalinkCreator: PropTypes.object.isRequired,
replyToEvent: PropTypes.object,
}; };
static contextType = MatrixClientContext; static contextType = MatrixClientContext;
@ -110,6 +109,8 @@ export default class SendMessageComposer extends React.Component {
cli.prepareToEncrypt(this.props.room); cli.prepareToEncrypt(this.props.room);
}, 60000); }, 60000);
} }
window.addEventListener("beforeunload", this._saveStoredEditorState);
} }
_setEditorRef = ref => { _setEditorRef = ref => {
@ -145,7 +146,7 @@ export default class SendMessageComposer extends React.Component {
if (e.shiftKey || e.metaKey) return; if (e.shiftKey || e.metaKey) return;
const shouldSelectHistory = e.altKey && e.ctrlKey; const shouldSelectHistory = e.altKey && e.ctrlKey;
const shouldEditLastMessage = !e.altKey && !e.ctrlKey && up && !RoomViewStore.getQuotingEvent(); const shouldEditLastMessage = !e.altKey && !e.ctrlKey && up && !this.props.replyToEvent;
if (shouldSelectHistory) { if (shouldSelectHistory) {
// Try select composer history // Try select composer history
@ -187,9 +188,13 @@ export default class SendMessageComposer extends React.Component {
this.sendHistoryManager.currentIndex = this.sendHistoryManager.history.length; this.sendHistoryManager.currentIndex = this.sendHistoryManager.history.length;
return; return;
} }
const serializedParts = this.sendHistoryManager.getItem(delta); const {parts, replyEventId} = this.sendHistoryManager.getItem(delta);
if (serializedParts) { dis.dispatch({
this.model.reset(serializedParts); action: 'reply_to_event',
event: replyEventId ? this.props.room.findEventById(replyEventId) : null,
});
if (parts) {
this.model.reset(parts);
this._editorRef.focus(); this._editorRef.focus();
} }
} }
@ -299,12 +304,12 @@ export default class SendMessageComposer extends React.Component {
} }
} }
const replyToEvent = this.props.replyToEvent;
if (shouldSend) { if (shouldSend) {
const isReply = !!RoomViewStore.getQuotingEvent();
const {roomId} = this.props.room; const {roomId} = this.props.room;
const content = createMessageContent(this.model, this.props.permalinkCreator); const content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent);
this.context.sendMessage(roomId, content); this.context.sendMessage(roomId, content);
if (isReply) { if (replyToEvent) {
// Clear reply_to_event as we put the message into the queue // Clear reply_to_event as we put the message into the queue
// if the send fails, retry will handle resending. // if the send fails, retry will handle resending.
dis.dispatch({ dis.dispatch({
@ -315,7 +320,7 @@ export default class SendMessageComposer extends React.Component {
dis.dispatch({action: "message_sent"}); dis.dispatch({action: "message_sent"});
} }
this.sendHistoryManager.save(this.model); this.sendHistoryManager.save(this.model, replyToEvent);
// clear composer // clear composer
this.model.reset([]); this.model.reset([]);
this._editorRef.clearUndoHistory(); this._editorRef.clearUndoHistory();
@ -325,6 +330,8 @@ export default class SendMessageComposer extends React.Component {
componentWillUnmount() { componentWillUnmount() {
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
window.removeEventListener("beforeunload", this._saveStoredEditorState);
this._saveStoredEditorState();
} }
// TODO: [REACT-WARNING] Move this to constructor // TODO: [REACT-WARNING] Move this to constructor
@ -347,8 +354,14 @@ export default class SendMessageComposer extends React.Component {
_restoreStoredEditorState(partCreator) { _restoreStoredEditorState(partCreator) {
const json = localStorage.getItem(this._editorStateKey); const json = localStorage.getItem(this._editorStateKey);
if (json) { if (json) {
const serializedParts = JSON.parse(json); const {parts: serializedParts, replyEventId} = SendHistoryManager.parseItem(JSON.parse(json));
const parts = serializedParts.map(p => partCreator.deserializePart(p)); const parts = serializedParts.map(p => partCreator.deserializePart(p));
if (replyEventId) {
dis.dispatch({
action: 'reply_to_event',
event: this.props.room.findEventById(replyEventId),
});
}
return parts; return parts;
} }
} }
@ -357,7 +370,8 @@ export default class SendMessageComposer extends React.Component {
if (this.model.isEmpty) { if (this.model.isEmpty) {
this._clearStoredEditorState(); this._clearStoredEditorState();
} else { } else {
localStorage.setItem(this._editorStateKey, JSON.stringify(this.model.serializeParts())); const item = SendHistoryManager.createItem(this.model, this.props.replyToEvent);
localStorage.setItem(this._editorStateKey, JSON.stringify(item));
} }
} }
@ -449,7 +463,6 @@ export default class SendMessageComposer extends React.Component {
room={this.props.room} room={this.props.room}
label={this.props.placeholder} label={this.props.placeholder}
placeholder={this.props.placeholder} placeholder={this.props.placeholder}
onChange={this._saveStoredEditorState}
onPaste={this._onPaste} onPaste={this._onPaste}
/> />
</div> </div>