Track replyToEvent along with CIDER state & history
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
45fa647655
commit
120f269190
4 changed files with 72 additions and 34 deletions
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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} />,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue