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

Track replyToEvent along with Cider state & history #5284

Merged
merged 9 commits into from
Oct 8, 2020
17 changes: 17 additions & 0 deletions __test-utils__/environment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const BaseEnvironment = require("jest-environment-jsdom-sixteen");

class Environment extends BaseEnvironment {
constructor(config, options) {
super(Object.assign({}, config, {
globals: Object.assign({}, config.globals, {
// Explicitly specify the correct globals to workaround Jest bug
// https://github.com/facebook/jest/issues/7780
Uint32Array: Uint32Array,
Uint8Array: Uint8Array,
ArrayBuffer: ArrayBuffer,
}),
}), options);
}
}

module.exports = Environment;
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@
"@babel/preset-typescript": "^7.10.4",
"@babel/register": "^7.10.5",
"@babel/traverse": "^7.11.0",
"@peculiar/webcrypto": "^1.1.2",
"@peculiar/webcrypto": "^1.1.3",
"@types/classnames": "^2.2.10",
"@types/counterpart": "^0.18.1",
"@types/flux": "^3.1.9",
Expand Down Expand Up @@ -151,8 +151,9 @@
"eslint-plugin-react": "^7.20.3",
"eslint-plugin-react-hooks": "^2.5.1",
"glob": "^5.0.15",
"jest": "^24.9.0",
"jest-canvas-mock": "^2.2.0",
"jest": "^26.5.2",
"jest-canvas-mock": "^2.3.0",
"jest-environment-jsdom-sixteen": "^1.0.3",
"lolex": "^5.1.2",
"matrix-mock-request": "^1.2.3",
"matrix-react-test-utils": "^0.2.2",
Expand All @@ -165,6 +166,7 @@
"walk": "^2.3.14"
},
"jest": {
"testEnvironment": "./__test-utils__/environment.js",
"testMatch": [
"<rootDir>/test/**/*-test.js"
],
Expand Down
35 changes: 25 additions & 10 deletions src/SendHistoryManager.js → src/SendHistoryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,21 @@ 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<HistoryItem> = [];
history: Array<IHistoryItem> = [];
prefix: string;
lastIndex: number = 0; // used for indexing the storage
currentIndex: number = 0; // used for indexing the loaded validated history Array
lastIndex = 0; // used for indexing the storage
currentIndex = 0; // used for indexing the loaded validated history Array

constructor(roomId: string, prefix: string) {
this.prefix = prefix + roomId;
Expand All @@ -32,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(JSON.parse(itemJSON));
} catch (e) {
console.warn("Throwing away unserialisable history", e);
break;
Expand All @@ -45,15 +53,22 @@ export default class SendHistoryManager {
this.currentIndex = this.lastIndex + 1;
}

save(editorModel: Object) {
const serializedParts = editorModel.serializeParts();
this.history.push(serializedParts);
static createItem(model: EditorModel, replyEvent?: MatrixEvent): IHistoryItem {
return {
parts: model.serializeParts(),
replyEventId: replyEvent ? replyEvent.getId() : undefined,
};
}

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(serializedParts));
sessionStorage.setItem(`${this.prefix}[${this.lastIndex}]`, JSON.stringify(item));
}

getItem(offset: number): ?HistoryItem {
getItem(offset: number): IHistoryItem {
this.currentIndex = clamp(this.currentIndex + offset, 0, this.history.length - 1);
return this.history[this.currentIndex];
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/rooms/BasicMessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ interface IProps {
label?: string;
initialCaret?: DocumentOffset;

onChange();
onChange?();
onPaste?(event: ClipboardEvent<HTMLDivElement>, model: EditorModel): boolean;
}

Expand Down
14 changes: 8 additions & 6 deletions src/components/views/rooms/MessageComposer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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}
/>,
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
);
Expand Down
65 changes: 40 additions & 25 deletions src/components/views/rooms/SendMessageComposer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -41,7 +40,6 @@ import {_t, _td} from '../../../languageHandler';
import ContentMessages from '../../../ContentMessages';
import {Key} from "../../../Keyboard";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import RateLimitedFunc from '../../../ratelimitedfunc';
import {Action} from "../../../dispatcher/actions";

Expand All @@ -61,7 +59,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);
Expand All @@ -70,21 +68,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;
Expand All @@ -95,6 +92,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;
Expand All @@ -104,12 +102,13 @@ export default class SendMessageComposer extends React.Component {
this.model = null;
this._editorRef = null;
this.currentlyComposedEditorState = null;
const cli = MatrixClientPeg.get();
if (cli.isCryptoEnabled() && cli.isRoomEncrypted(this.props.room.roomId)) {
if (this.context.isCryptoEnabled() && this.context.isRoomEncrypted(this.props.room.roomId)) {
this._prepareToEncrypt = new RateLimitedFunc(() => {
cli.prepareToEncrypt(this.props.room);
this.context.prepareToEncrypt(this.props.room);
}, 60000);
}

window.addEventListener("beforeunload", this._saveStoredEditorState);
}

_setEditorRef = ref => {
Expand Down Expand Up @@ -145,7 +144,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
Expand Down Expand Up @@ -187,9 +186,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();
}
}
Expand Down Expand Up @@ -299,12 +302,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({
Expand All @@ -315,7 +318,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();
Expand All @@ -325,6 +328,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
Expand All @@ -333,11 +338,11 @@ export default class SendMessageComposer extends React.Component {
const parts = this._restoreStoredEditorState(partCreator) || [];
this.model = new EditorModel(parts, partCreator);
this.dispatcherRef = dis.register(this.onAction);
this.sendHistoryManager = new SendHistoryManager(this.props.room.roomId, 'mx_cider_composer_history_');
this.sendHistoryManager = new SendHistoryManager(this.props.room.roomId, 'mx_cider_history_');
}

get _editorStateKey() {
return `cider_editor_state_${this.props.room.roomId}`;
return `mx_cider_state_${this.props.room.roomId}`;
}

_clearStoredEditorState() {
Expand All @@ -347,17 +352,28 @@ 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.map(p => partCreator.deserializePart(p));
return parts;
try {
const {parts: serializedParts, replyEventId} = 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;
} catch (e) {
console.error(e);
}
}
}

_saveStoredEditorState = () => {
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));
}
}

Expand Down Expand Up @@ -449,7 +465,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}
/>
</div>
Expand Down
Loading