diff --git a/src/SendHistoryManager.ts b/src/SendHistoryManager.ts index 106fcb51fb..8e4903a616 100644 --- a/src/SendHistoryManager.ts +++ b/src/SendHistoryManager.ts @@ -16,11 +16,18 @@ limitations under the License. */ import {clamp} from "lodash"; +import {MatrixEvent} from "matrix-js-sdk/src/models/event"; + import {SerializedPart} from "./editor/parts"; import EditorModel from "./editor/model"; +interface IHistoryItem { + parts: SerializedPart[]; + replyEventId?: string; +} + export default class SendHistoryManager { - history: Array = []; + history: Array = []; prefix: string; lastIndex = 0; // used for indexing the storage 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}]`)) { try { - const serializedParts = JSON.parse(itemJSON); - this.history.push(serializedParts); + this.history.push(SendHistoryManager.parseItem(JSON.parse(itemJSON))); } catch (e) { console.warn("Throwing away unserialisable history", e); break; @@ -47,15 +53,32 @@ export default class SendHistoryManager { this.currentIndex = this.lastIndex + 1; } - save(editorModel: EditorModel) { - const serializedParts = editorModel.serializeParts(); - this.history.push(serializedParts); - this.currentIndex = this.history.length; - this.lastIndex += 1; - sessionStorage.setItem(`${this.prefix}[${this.lastIndex}]`, JSON.stringify(serializedParts)); + static createItem(model: EditorModel, replyEvent?: MatrixEvent): IHistoryItem { + return { + parts: model.serializeParts(), + replyEventId: replyEvent ? replyEvent.getId() : undefined, + }; } - 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); return this.history[this.currentIndex]; } diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index d9b34b93ef..311a4734fd 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -92,7 +92,7 @@ interface IProps { label?: string; initialCaret?: DocumentOffset; - onChange(); + onChange?(); onPaste?(event: ClipboardEvent, model: EditorModel): boolean; } diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 71999fb04f..33e167b6dd 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -257,7 +257,7 @@ export default class MessageComposer extends React.Component { this._dispatcherRef = null; this.state = { - isQuoting: Boolean(RoomViewStore.getQuotingEvent()), + replyToEvent: RoomViewStore.getQuotingEvent(), tombstone: this._getRoomTombstone(), canSendMessages: this.props.room.maySendMessage(), showCallButtons: SettingsStore.getValue("showCallButtonsInComposer"), @@ -337,9 +337,9 @@ export default class MessageComposer extends React.Component { } _onRoomViewStoreUpdate() { - const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); - if (this.state.isQuoting === isQuoting) return; - this.setState({ isQuoting }); + const replyToEvent = RoomViewStore.getQuotingEvent(); + if (this.state.replyToEvent === replyToEvent) return; + this.setState({ replyToEvent }); } onInputStateChanged(inputState) { @@ -378,7 +378,7 @@ export default class MessageComposer extends React.Component { } renderPlaceholderText() { - if (this.state.isQuoting) { + if (this.state.replyToEvent) { if (this.props.e2eStatus) { return _t('Send an encrypted reply…'); } else { @@ -423,7 +423,9 @@ export default class MessageComposer extends React.Component { room={this.props.room} placeholder={this.renderPlaceholderText()} resizeNotifier={this.props.resizeNotifier} - permalinkCreator={this.props.permalinkCreator} />, + permalinkCreator={this.props.permalinkCreator} + replyToEvent={this.state.replyToEvent} + />, , , ); diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 25dcf8ccd5..029a21b519 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -29,7 +29,6 @@ import { } from '../../../editor/serialize'; import {CommandPartCreator} from '../../../editor/parts'; import BasicMessageComposer from "./BasicMessageComposer"; -import RoomViewStore from '../../../stores/RoomViewStore'; import ReplyThread from "../elements/ReplyThread"; import {parseEvent} from '../../../editor/deserialize'; import {findEditableEvent} from '../../../utils/EventUtils'; @@ -61,7 +60,7 @@ function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { } // exported for tests -export function createMessageContent(model, permalinkCreator) { +export function createMessageContent(model, permalinkCreator, replyToEvent) { const isEmote = containsEmote(model); if (isEmote) { model = stripEmoteCommand(model); @@ -70,21 +69,20 @@ export function createMessageContent(model, permalinkCreator) { model = stripPrefix(model, "/"); } model = unescapeMessage(model); - const repliedToEvent = RoomViewStore.getQuotingEvent(); const body = textSerialize(model); const content = { msgtype: isEmote ? "m.emote" : "m.text", body: body, }; - const formattedBody = htmlSerializeIfNeeded(model, {forceHTML: !!repliedToEvent}); + const formattedBody = htmlSerializeIfNeeded(model, {forceHTML: !!replyToEvent}); if (formattedBody) { content.format = "org.matrix.custom.html"; content.formatted_body = formattedBody; } - if (repliedToEvent) { - addReplyToMessageContent(content, repliedToEvent, permalinkCreator); + if (replyToEvent) { + addReplyToMessageContent(content, replyToEvent, permalinkCreator); } return content; @@ -95,6 +93,7 @@ export default class SendMessageComposer extends React.Component { room: PropTypes.object.isRequired, placeholder: PropTypes.string, permalinkCreator: PropTypes.object.isRequired, + replyToEvent: PropTypes.object, }; static contextType = MatrixClientContext; @@ -110,6 +109,8 @@ export default class SendMessageComposer extends React.Component { cli.prepareToEncrypt(this.props.room); }, 60000); } + + window.addEventListener("beforeunload", this._saveStoredEditorState); } _setEditorRef = ref => { @@ -145,7 +146,7 @@ export default class SendMessageComposer extends React.Component { if (e.shiftKey || e.metaKey) return; 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) { // Try select composer history @@ -187,9 +188,13 @@ export default class SendMessageComposer extends React.Component { this.sendHistoryManager.currentIndex = this.sendHistoryManager.history.length; return; } - const serializedParts = this.sendHistoryManager.getItem(delta); - if (serializedParts) { - this.model.reset(serializedParts); + const {parts, replyEventId} = this.sendHistoryManager.getItem(delta); + dis.dispatch({ + action: 'reply_to_event', + event: replyEventId ? this.props.room.findEventById(replyEventId) : null, + }); + if (parts) { + this.model.reset(parts); this._editorRef.focus(); } } @@ -299,12 +304,12 @@ export default class SendMessageComposer extends React.Component { } } + const replyToEvent = this.props.replyToEvent; if (shouldSend) { - const isReply = !!RoomViewStore.getQuotingEvent(); 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); - if (isReply) { + if (replyToEvent) { // Clear reply_to_event as we put the message into the queue // if the send fails, retry will handle resending. dis.dispatch({ @@ -315,7 +320,7 @@ export default class SendMessageComposer extends React.Component { dis.dispatch({action: "message_sent"}); } - this.sendHistoryManager.save(this.model); + this.sendHistoryManager.save(this.model, replyToEvent); // clear composer this.model.reset([]); this._editorRef.clearUndoHistory(); @@ -325,6 +330,8 @@ export default class SendMessageComposer extends React.Component { componentWillUnmount() { dis.unregister(this.dispatcherRef); + window.removeEventListener("beforeunload", this._saveStoredEditorState); + this._saveStoredEditorState(); } // TODO: [REACT-WARNING] Move this to constructor @@ -347,8 +354,14 @@ export default class SendMessageComposer extends React.Component { _restoreStoredEditorState(partCreator) { const json = localStorage.getItem(this._editorStateKey); if (json) { - const serializedParts = JSON.parse(json); + const {parts: serializedParts, replyEventId} = SendHistoryManager.parseItem(JSON.parse(json)); const parts = serializedParts.map(p => partCreator.deserializePart(p)); + if (replyEventId) { + dis.dispatch({ + action: 'reply_to_event', + event: this.props.room.findEventById(replyEventId), + }); + } return parts; } } @@ -357,7 +370,8 @@ export default class SendMessageComposer extends React.Component { if (this.model.isEmpty) { this._clearStoredEditorState(); } 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} label={this.props.placeholder} placeholder={this.props.placeholder} - onChange={this._saveStoredEditorState} onPaste={this._onPaste} />