mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-15 20:54:59 +08:00
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/feat/widgets
Conflicts: src/components/views/elements/AppTile.js
This commit is contained in:
commit
317dc68135
17
__test-utils__/environment.js
Normal file
17
__test-utils__/environment.js
Normal file
@ -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;
|
10
package.json
10
package.json
@ -79,7 +79,7 @@
|
||||
"linkifyjs": "^2.1.9",
|
||||
"lodash": "^4.17.19",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
"matrix-widget-api": "^0.1.0-beta.2",
|
||||
"matrix-widget-api": "^0.1.0-beta.3",
|
||||
"minimist": "^1.2.5",
|
||||
"pako": "^1.0.11",
|
||||
"parse5": "^5.1.1",
|
||||
@ -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",
|
||||
@ -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",
|
||||
@ -165,6 +166,7 @@
|
||||
"walk": "^2.3.14"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "./__test-utils__/environment.js",
|
||||
"testMatch": [
|
||||
"<rootDir>/test/**/*-test.js"
|
||||
],
|
||||
|
@ -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;
|
||||
@ -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;
|
||||
@ -45,15 +53,22 @@ export default class SendHistoryManager {
|
||||
this.currentIndex = this.lastIndex + 1;
|
||||
}
|
||||
|
||||
save(editorModel: Object) {
|
||||
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): ?HistoryItem {
|
||||
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];
|
||||
}
|
@ -166,7 +166,8 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({children, handleHomeEn
|
||||
|
||||
const onKeyDownHandler = useCallback((ev) => {
|
||||
let handled = false;
|
||||
if (handleHomeEnd) {
|
||||
// Don't interfere with input default keydown behaviour
|
||||
if (handleHomeEnd && ev.target.tagName !== "INPUT") {
|
||||
// check if we actually have any items
|
||||
switch (ev.key) {
|
||||
case Key.HOME:
|
||||
|
@ -28,6 +28,9 @@ interface IProps extends Omit<React.HTMLProps<HTMLDivElement>, "onKeyDown"> {
|
||||
const Toolbar: React.FC<IProps> = ({children, ...props}) => {
|
||||
const onKeyDown = (ev: React.KeyboardEvent, state: IState) => {
|
||||
const target = ev.target as HTMLElement;
|
||||
// Don't interfere with input default keydown behaviour
|
||||
if (target.tagName === "INPUT") return;
|
||||
|
||||
let handled = true;
|
||||
|
||||
// HOME and END are handled by RovingTabIndexProvider
|
||||
|
@ -47,6 +47,7 @@ export default class AppTile extends React.Component {
|
||||
// The key used for PersistedElement
|
||||
this._persistKey = getPersistKey(this.props.app.id);
|
||||
this._sgWidget = new StopGapWidget(this.props);
|
||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
||||
this._sgWidget.on("ready", this._onWidgetReady);
|
||||
this.iframe = null; // ref to the iframe (callback style)
|
||||
|
||||
@ -95,9 +96,7 @@ export default class AppTile extends React.Component {
|
||||
this._sgWidget.stop();
|
||||
}
|
||||
|
||||
this.setState({
|
||||
hasPermissionToLoad,
|
||||
});
|
||||
this.setState({ hasPermissionToLoad });
|
||||
};
|
||||
|
||||
isMixedContent() {
|
||||
@ -144,6 +143,7 @@ export default class AppTile extends React.Component {
|
||||
this._sgWidget.stop();
|
||||
}
|
||||
this._sgWidget = new StopGapWidget(newProps);
|
||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
||||
this._sgWidget.on("ready", this._onWidgetReady);
|
||||
this._startWidget();
|
||||
}
|
||||
@ -204,8 +204,11 @@ export default class AppTile extends React.Component {
|
||||
this._sgWidget.stop();
|
||||
}
|
||||
|
||||
_onWidgetReady = () => {
|
||||
_onWidgetPrepared = () => {
|
||||
this.setState({loading: false});
|
||||
};
|
||||
|
||||
_onWidgetReady = () => {
|
||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||
this._sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {});
|
||||
}
|
||||
|
@ -80,27 +80,30 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
|
||||
|
||||
private fakeEvent({userId, displayname, avatar_url: avatarUrl}: IState) {
|
||||
// Fake it till we make it
|
||||
const event = new MatrixEvent(JSON.parse(`{
|
||||
"type": "m.room.message",
|
||||
"sender": "${userId}",
|
||||
"content": {
|
||||
/* eslint-disable quote-props */
|
||||
const rawEvent = {
|
||||
type: "m.room.message",
|
||||
sender: userId,
|
||||
content: {
|
||||
"m.new_content": {
|
||||
"msgtype": "m.text",
|
||||
"body": "${this.props.message}",
|
||||
"displayname": "${displayname}",
|
||||
"avatar_url": "${avatarUrl}"
|
||||
msgtype: "m.text",
|
||||
body: this.props.message,
|
||||
displayname: displayname,
|
||||
avatar_url: avatarUrl,
|
||||
},
|
||||
"msgtype": "m.text",
|
||||
"body": "${this.props.message}",
|
||||
"displayname": "${displayname}",
|
||||
"avatar_url": "${avatarUrl}"
|
||||
msgtype: "m.text",
|
||||
body: this.props.message,
|
||||
displayname: displayname,
|
||||
avatar_url: avatarUrl,
|
||||
},
|
||||
"unsigned": {
|
||||
"age": 97
|
||||
unsigned: {
|
||||
age: 97,
|
||||
},
|
||||
"event_id": "$9999999999999999999999999999999999999999999",
|
||||
"room_id": "!999999999999999999:matrix.org"
|
||||
}`));
|
||||
event_id: "$9999999999999999999999999999999999999999999",
|
||||
room_id: "!999999999999999999:example.org",
|
||||
};
|
||||
const event = new MatrixEvent(rawEvent);
|
||||
/* eslint-enable quote-props */
|
||||
|
||||
// Fake it more
|
||||
event.sender = {
|
||||
|
@ -92,7 +92,7 @@ interface IProps {
|
||||
label?: string;
|
||||
initialCaret?: DocumentOffset;
|
||||
|
||||
onChange();
|
||||
onChange?();
|
||||
onPaste?(event: ClipboardEvent<HTMLDivElement>, model: EditorModel): boolean;
|
||||
}
|
||||
|
||||
|
@ -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}
|
||||
/>,
|
||||
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
|
||||
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
|
||||
);
|
||||
|
@ -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';
|
||||
@ -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";
|
||||
|
||||
@ -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);
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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 => {
|
||||
@ -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
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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({
|
||||
@ -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();
|
||||
@ -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
|
||||
@ -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() {
|
||||
@ -347,9 +352,19 @@ export default class SendMessageComposer extends React.Component {
|
||||
_restoreStoredEditorState(partCreator) {
|
||||
const json = localStorage.getItem(this._editorStateKey);
|
||||
if (json) {
|
||||
const serializedParts = JSON.parse(json);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -357,7 +372,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 +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>
|
||||
|
@ -163,6 +163,7 @@ export class StopGapWidget extends EventEmitter {
|
||||
if (this.started) return;
|
||||
const driver = new StopGapWidgetDriver( this.appTileProps.whitelistCapabilities || []);
|
||||
this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver);
|
||||
this.messaging.addEventListener("preparing", () => this.emit("preparing"));
|
||||
this.messaging.addEventListener("ready", () => this.emit("ready"));
|
||||
WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging);
|
||||
|
||||
|
@ -14,16 +14,28 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import RoomViewStore from "../../../../src/stores/RoomViewStore";
|
||||
import {createMessageContent} from "../../../../src/components/views/rooms/SendMessageComposer";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
import { configure, mount } from "enzyme";
|
||||
import React from "react";
|
||||
import {act} from "react-dom/test-utils";
|
||||
|
||||
import SendMessageComposer, {createMessageContent} from "../../../../src/components/views/rooms/SendMessageComposer";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import EditorModel from "../../../../src/editor/model";
|
||||
import {createPartCreator, createRenderer} from "../../../editor/mock";
|
||||
import {createTestClient, mkEvent, mkStubRoom} from "../../../test-utils";
|
||||
import BasicMessageComposer from "../../../../src/components/views/rooms/BasicMessageComposer";
|
||||
import {MatrixClientPeg} from "../../../../src/MatrixClientPeg";
|
||||
import {sleep} from "../../../../src/utils/promise";
|
||||
import SpecPermalinkConstructor from "../../../../src/utils/permalinks/SpecPermalinkConstructor";
|
||||
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
|
||||
|
||||
jest.mock("../../../../src/stores/RoomViewStore");
|
||||
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
describe('<SendMessageComposer/>', () => {
|
||||
describe("createMessageContent", () => {
|
||||
RoomViewStore.getQuotingEvent.mockReturnValue(false);
|
||||
const permalinkCreator = jest.fn();
|
||||
|
||||
it("sends plaintext messages correctly", () => {
|
||||
@ -78,6 +90,143 @@ describe('<SendMessageComposer/>', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("functions correctly mounted", () => {
|
||||
const mockClient = MatrixClientPeg.matrixClient = createTestClient();
|
||||
const mockRoom = mkStubRoom();
|
||||
const mockEvent = mkEvent({
|
||||
type: "m.room.message",
|
||||
content: "Replying to this",
|
||||
event: true,
|
||||
});
|
||||
mockRoom.findEventById = jest.fn(eventId => {
|
||||
return eventId === mockEvent.getId() ? mockEvent : null;
|
||||
});
|
||||
|
||||
const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch");
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
spyDispatcher.mockReset();
|
||||
});
|
||||
|
||||
it("renders text and placeholder correctly", () => {
|
||||
const wrapper = mount(<MatrixClientContext.Provider value={mockClient}>
|
||||
<SendMessageComposer
|
||||
room={mockRoom}
|
||||
placeholder="placeholder string"
|
||||
permalinkCreator={new SpecPermalinkConstructor()}
|
||||
/>
|
||||
</MatrixClientContext.Provider>);
|
||||
|
||||
expect(wrapper.find('[aria-label="placeholder string"]')).toHaveLength(1);
|
||||
|
||||
act(() => {
|
||||
wrapper.find(BasicMessageComposer).instance().insertText("Test Text");
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toBe("Test Text");
|
||||
});
|
||||
|
||||
it("correctly persists state to and from localStorage", () => {
|
||||
const wrapper = mount(<MatrixClientContext.Provider value={mockClient}>
|
||||
<SendMessageComposer
|
||||
room={mockRoom}
|
||||
placeholder=""
|
||||
permalinkCreator={new SpecPermalinkConstructor()}
|
||||
replyToEvent={mockEvent}
|
||||
/>
|
||||
</MatrixClientContext.Provider>);
|
||||
|
||||
act(() => {
|
||||
wrapper.find(BasicMessageComposer).instance().insertText("Test Text");
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
const key = wrapper.find(SendMessageComposer).instance()._editorStateKey;
|
||||
|
||||
expect(wrapper.text()).toBe("Test Text");
|
||||
expect(localStorage.getItem(key)).toBeNull();
|
||||
|
||||
// ensure the right state was persisted to localStorage
|
||||
wrapper.unmount();
|
||||
expect(JSON.parse(localStorage.getItem(key))).toStrictEqual({
|
||||
parts: [{"type": "plain", "text": "Test Text"}],
|
||||
replyEventId: mockEvent.getId(),
|
||||
});
|
||||
|
||||
// ensure the correct model is re-loaded
|
||||
wrapper.mount();
|
||||
expect(wrapper.text()).toBe("Test Text");
|
||||
expect(spyDispatcher).toHaveBeenCalledWith({
|
||||
action: "reply_to_event",
|
||||
event: mockEvent,
|
||||
});
|
||||
|
||||
// now try with localStorage wiped out
|
||||
wrapper.unmount();
|
||||
localStorage.removeItem(key);
|
||||
wrapper.mount();
|
||||
expect(wrapper.text()).toBe("");
|
||||
});
|
||||
|
||||
it("persists state correctly without replyToEvent onbeforeunload", () => {
|
||||
const wrapper = mount(<MatrixClientContext.Provider value={mockClient}>
|
||||
<SendMessageComposer
|
||||
room={mockRoom}
|
||||
placeholder=""
|
||||
permalinkCreator={new SpecPermalinkConstructor()}
|
||||
/>
|
||||
</MatrixClientContext.Provider>);
|
||||
|
||||
act(() => {
|
||||
wrapper.find(BasicMessageComposer).instance().insertText("Hello World");
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
const key = wrapper.find(SendMessageComposer).instance()._editorStateKey;
|
||||
|
||||
expect(wrapper.text()).toBe("Hello World");
|
||||
expect(localStorage.getItem(key)).toBeNull();
|
||||
|
||||
// ensure the right state was persisted to localStorage
|
||||
window.dispatchEvent(new Event('beforeunload'));
|
||||
expect(JSON.parse(localStorage.getItem(key))).toStrictEqual({
|
||||
parts: [{"type": "plain", "text": "Hello World"}],
|
||||
});
|
||||
});
|
||||
|
||||
it("persists to session history upon sending", async () => {
|
||||
const wrapper = mount(<MatrixClientContext.Provider value={mockClient}>
|
||||
<SendMessageComposer
|
||||
room={mockRoom}
|
||||
placeholder="placeholder"
|
||||
permalinkCreator={new SpecPermalinkConstructor()}
|
||||
replyToEvent={mockEvent}
|
||||
/>
|
||||
</MatrixClientContext.Provider>);
|
||||
|
||||
act(() => {
|
||||
wrapper.find(BasicMessageComposer).instance().insertText("This is a message");
|
||||
wrapper.find(".mx_SendMessageComposer").simulate("keydown", { key: "Enter" });
|
||||
wrapper.update();
|
||||
});
|
||||
await sleep(10); // await the async _sendMessage
|
||||
wrapper.update();
|
||||
expect(spyDispatcher).toHaveBeenCalledWith({
|
||||
action: "reply_to_event",
|
||||
event: null,
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toBe("");
|
||||
const str = sessionStorage.getItem(`mx_cider_history_${mockRoom.roomId}[0]`);
|
||||
expect(JSON.parse(str)).toStrictEqual({
|
||||
parts: [{"type": "plain", "text": "This is a message"}],
|
||||
replyEventId: mockEvent.getId(),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
@ -80,6 +80,7 @@ export function createTestClient() {
|
||||
getSyncState: () => "SYNCING",
|
||||
generateClientSecret: () => "t35tcl1Ent5ECr3T",
|
||||
isGuest: () => false,
|
||||
isCryptoEnabled: () => false,
|
||||
};
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user