Merge pull request #9374 from matrix-org/feat/matrix-wysisyg-integration

First step of matrix-wysiwyg integration
This commit is contained in:
Florian Duros 2022-10-11 11:08:15 +02:00 committed by GitHub
commit b336e18eae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 693 additions and 15 deletions

View file

@ -57,6 +57,7 @@
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/analytics-events": "^0.2.0",
"@matrix-org/matrix-wysiwyg": "^0.0.2",
"@matrix-org/react-sdk-module-api": "^0.0.3",
"@sentry/browser": "^6.11.0",
"@sentry/tracing": "^6.11.0",

View file

@ -295,6 +295,7 @@
@import "./views/rooms/_TopUnreadMessagesBar.pcss";
@import "./views/rooms/_VoiceRecordComposerTile.pcss";
@import "./views/rooms/_WhoIsTypingTile.pcss";
@import "./views/rooms/wysiwyg_composer/_WysiwygComposer.pcss";
@import "./views/settings/_AvatarSetting.pcss";
@import "./views/settings/_CrossSigningPanel.pcss";
@import "./views/settings/_CryptographyPanel.pcss";

View file

@ -0,0 +1,53 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_WysiwygComposer {
flex: 1;
display: flex;
flex-direction: column;
font-size: $font-14px;
/* fixed line height to prevent emoji from being taller than text */
line-height: $font-18px;
justify-content: center;
margin-right: 6px;
/* don't grow wider than available space */
min-width: 0;
.mx_WysiwygComposer_container {
flex: 1;
display: flex;
flex-direction: column;
/* min-height at this level so the mx_BasicMessageComposer_input */
/* still stays vertically centered when less than 55px. */
/* We also set this to ensure the voice message recording widget */
/* doesn't cause a jump. */
min-height: 55px;
.mx_WysiwygComposer_content {
border: 1px solid;
border-radius: 20px;
padding: 8px 10px;
/* this will center the contenteditable */
/* in it's parent vertically */
/* while keeping the autocomplete at the top */
/* of the composer. The parent needs to be a flex container for this to work. */
margin: auto 0;
/* max-height at this level so autocomplete doesn't get scrolled too */
max-height: 140px;
overflow-y: auto;
}
}
}

View file

@ -58,6 +58,7 @@ import {
startNewVoiceBroadcastRecording,
VoiceBroadcastRecordingsStore,
} from '../../../voice-broadcast';
import { WysiwygComposer } from './wysiwyg_composer/WysiwygComposer';
let instanceCount = 0;
@ -105,6 +106,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
private voiceRecordingButton = createRef<VoiceRecordComposerTile>();
private ref: React.RefObject<HTMLDivElement> = createRef();
private instanceId: number;
private composerSendMessage?: () => void;
private _voiceRecording: Optional<VoiceMessageRecording>;
@ -313,6 +315,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
}
this.messageComposerInput.current?.sendMessage();
this.composerSendMessage?.();
};
private onChange = (model: EditorModel) => {
@ -321,6 +324,12 @@ export default class MessageComposer extends React.Component<IProps, IState> {
});
};
private onWysiwygChange = (content: string) => {
this.setState({
isComposerEmpty: content?.length === 0,
});
};
private onVoiceStoreUpdate = () => {
this.updateRecordingState();
};
@ -394,20 +403,37 @@ export default class MessageComposer extends React.Component<IProps, IState> {
const canSendMessages = this.context.canSendMessages && !this.context.tombstone;
if (canSendMessages) {
controls.push(
<SendMessageComposer
ref={this.messageComposerInput}
key="controls_input"
room={this.props.room}
placeholder={this.renderPlaceholderText()}
permalinkCreator={this.props.permalinkCreator}
relation={this.props.relation}
replyToEvent={this.props.replyToEvent}
onChange={this.onChange}
disabled={this.state.haveRecording}
toggleStickerPickerOpen={this.toggleStickerPickerOpen}
/>,
);
const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer");
if (isWysiwygComposerEnabled) {
controls.push(
<WysiwygComposer key="controls_input"
disabled={this.state.haveRecording}
onChange={this.onWysiwygChange}
permalinkCreator={this.props.permalinkCreator}
relation={this.props.relation}
replyToEvent={this.props.replyToEvent}>
{ (sendMessage) => {
this.composerSendMessage = sendMessage;
} }
</WysiwygComposer>,
);
} else {
controls.push(
<SendMessageComposer
ref={this.messageComposerInput}
key="controls_input"
room={this.props.room}
placeholder={this.renderPlaceholderText()}
permalinkCreator={this.props.permalinkCreator}
relation={this.props.relation}
replyToEvent={this.props.replyToEvent}
onChange={this.onChange}
disabled={this.state.haveRecording}
toggleStickerPickerOpen={this.toggleStickerPickerOpen}
/>,
);
}
controls.push(<VoiceRecordComposerTile
key="controls_voice_record"

View file

@ -0,0 +1,71 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useState } from 'react';
import { useWysiwyg } from "@matrix-org/matrix-wysiwyg";
import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { useRoomContext } from '../../../../contexts/RoomContext';
import { sendMessage } from './message';
import { RoomPermalinkCreator } from '../../../../utils/permalinks/Permalinks';
import { useMatrixClientContext } from '../../../../contexts/MatrixClientContext';
interface WysiwygProps {
disabled?: boolean;
onChange: (content: string) => void;
relation?: IEventRelation;
replyToEvent?: MatrixEvent;
permalinkCreator: RoomPermalinkCreator;
includeReplyLegacyFallback?: boolean;
children?: (sendMessage: () => void) => void;
}
export function WysiwygComposer(
{ disabled = false, onChange, children, ...props }: WysiwygProps,
) {
const roomContext = useRoomContext();
const mxClient = useMatrixClientContext();
const [content, setContent] = useState<string>();
const { ref, isWysiwygReady, wysiwyg } = useWysiwyg({ onChange: (_content) => {
setContent(_content);
onChange(_content);
} });
const memoizedSendMessage = useCallback(() => {
sendMessage(content, { mxClient, roomContext, ...props });
wysiwyg.clear();
ref.current?.focus();
}, [content, mxClient, roomContext, wysiwyg, props, ref]);
return (
<div className="mx_WysiwygComposer">
<div className="mx_WysiwygComposer_container">
<div className="mx_WysiwygComposer_content"
ref={ref}
contentEditable={!disabled && isWysiwygReady}
role="textbox"
aria-multiline="true"
aria-autocomplete="list"
aria-haspopup="listbox"
dir="auto"
aria-disabled={disabled || !isWysiwygReady}
/>
</div>
{ children?.(memoizedSendMessage) }
</div>
);
}

View file

@ -0,0 +1,190 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Composer as ComposerEvent } from "@matrix-org/analytics-events/types/typescript/Composer";
import { IContent, IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
import { PosthogAnalytics } from "../../../../PosthogAnalytics";
import SettingsStore from "../../../../settings/SettingsStore";
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../../sendTimePerformanceMetrics";
import { attachRelation } from "../SendMessageComposer";
import { RoomPermalinkCreator } from "../../../../utils/permalinks/Permalinks";
import { doMaybeLocalRoomAction } from "../../../../utils/local-room";
import { CHAT_EFFECTS } from "../../../../effects";
import { containsEmoji } from "../../../../effects/utils";
import { IRoomState } from "../../../structures/RoomView";
import dis from '../../../../dispatcher/dispatcher';
interface SendMessageParams {
mxClient: MatrixClient;
relation?: IEventRelation;
replyToEvent?: MatrixEvent;
roomContext: IRoomState;
permalinkCreator: RoomPermalinkCreator;
includeReplyLegacyFallback?: boolean;
}
// exported for tests
export function createMessageContent(
message: string,
{ relation, replyToEvent, permalinkCreator, includeReplyLegacyFallback = true }:
Omit<SendMessageParams, 'roomContext' | 'mxClient'>,
): IContent {
// TODO emote ?
/*const isEmote = containsEmote(model);
if (isEmote) {
model = stripEmoteCommand(model);
}
if (startsWith(model, "//")) {
model = stripPrefix(model, "/");
}
model = unescapeMessage(model);*/
// const body = textSerialize(model);
const body = message;
const content: IContent = {
// TODO emote
// msgtype: isEmote ? "m.emote" : "m.text",
msgtype: "m.text",
body: body,
};
// TODO markdown support
/*const formattedBody = htmlSerializeIfNeeded(model, {
forceHTML: !!replyToEvent,
useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
});*/
const formattedBody = message;
if (formattedBody) {
content.format = "org.matrix.custom.html";
content.formatted_body = formattedBody;
}
attachRelation(content, relation);
// TODO reply
/*if (replyToEvent) {
addReplyToMessageContent(content, replyToEvent, {
permalinkCreator,
includeLegacyFallback: includeReplyLegacyFallback,
});
}*/
return content;
}
export function sendMessage(
message: string,
{ roomContext, mxClient, ...params }: SendMessageParams,
) {
const { relation, replyToEvent } = params;
const { room } = roomContext;
const { roomId } = room;
const posthogEvent: ComposerEvent = {
eventName: "Composer",
isEditing: false,
isReply: Boolean(replyToEvent),
inThread: relation?.rel_type === THREAD_RELATION_TYPE.name,
};
// TODO thread
/*if (posthogEvent.inThread) {
const threadRoot = room.findEventById(relation?.event_id);
posthogEvent.startsThread = threadRoot?.getThread()?.events.length === 1;
}*/
PosthogAnalytics.instance.trackEvent<ComposerEvent>(posthogEvent);
let content: IContent;
// TODO slash comment
// TODO replace emotion end of message ?
// TODO quick reaction
if (!content) {
content = createMessageContent(
message,
params,
);
}
// don't bother sending an empty message
if (!content.body.trim()) {
return;
}
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
decorateStartSendingTime(content);
}
const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name
? relation.event_id
: null;
const prom = doMaybeLocalRoomAction(
roomId,
(actualRoomId: string) => mxClient.sendMessage(actualRoomId, threadId, content),
mxClient,
);
// TODO reply
/*if (replyToEvent) {
// Clear reply_to_event as we put the message into the queue
// if the send fails, retry will handle resending.
dis.dispatch({
action: 'reply_to_event',
event: null,
context: roomContext.timelineRenderingType,
});
}*/
dis.dispatch({ action: "message_sent" });
CHAT_EFFECTS.forEach((effect) => {
if (containsEmoji(content, effect.emojis)) {
// For initial threads launch, chat effects are disabled
// see #19731
const isNotThread = relation?.rel_type !== THREAD_RELATION_TYPE.name;
if (!SettingsStore.getValue("feature_thread") || isNotThread) {
dis.dispatch({ action: `effects.${effect.command}` });
}
}
});
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
prom.then(resp => {
sendRoundTripMetric(mxClient, roomId, resp.event_id);
});
}
// TODO save history
// TODO save local state
//if (shouldSend && SettingsStore.getValue("scrollToBottomOnMessageSent")) {
if (SettingsStore.getValue("scrollToBottomOnMessageSent")) {
dis.dispatch({
action: "scroll_to_bottom",
timelineRenderingType: roomContext.timelineRenderingType,
});
}
return prom;
}

View file

@ -25,6 +25,10 @@ export interface MatrixClientProps {
mxClient: MatrixClient;
}
export function useMatrixClientContext() {
return useContext(MatrixClientContext);
}
const matrixHOC = <ComposedComponentProps extends {}>(
ComposedComponent: ComponentClass<ComposedComponentProps>,
) => {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { createContext } from "react";
import { createContext, useContext } from "react";
import { IRoomState } from "../components/structures/RoomView";
import { Layout } from "../settings/enums/Layout";
@ -69,3 +69,6 @@ const RoomContext = createContext<IRoomState>({
});
RoomContext.displayName = "RoomContext";
export default RoomContext;
export function useRoomContext() {
return useContext(RoomContext);
}

View file

@ -901,6 +901,7 @@
"How can I leave the beta?": "How can I leave the beta?",
"To leave, return to this page and use the “%(leaveTheBeta)s” button.": "To leave, return to this page and use the “%(leaveTheBeta)s” button.",
"Leave the beta": "Leave the beta",
"Wysiwyg composer (plain text mode coming soon) (under active development)": "Wysiwyg composer (plain text mode coming soon) (under active development)",
"Render simple counters in room header": "Render simple counters in room header",
"Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)",
"Support adding custom themes": "Support adding custom themes",

View file

@ -303,6 +303,13 @@ export const SETTINGS: {[setting: string]: ISetting} = {
},
},
"feature_wysiwyg_composer": {
isFeature: true,
labsGroup: LabGroup.Messaging,
displayName: _td("Wysiwyg composer (plain text mode coming soon) (under active development)"),
supportedLevels: LEVELS_FEATURE,
default: false,
},
"feature_state_counters": {
isFeature: true,
labsGroup: LabGroup.Rooms,

View file

@ -39,6 +39,15 @@ import { SendMessageComposer } from "../../../../src/components/views/rooms/Send
import { E2EStatus } from "../../../../src/utils/ShieldUtils";
import { addTextToComposer } from "../../../test-utils/composer";
import UIStore, { UI_EVENTS } from "../../../../src/stores/UIStore";
import { WysiwygComposer } from "../../../../src/components/views/rooms/wysiwyg_composer/WysiwygComposer";
// The wysiwyg fetch wasm bytes and a specific workaround is needed to make it works in a node (jest) environnement
// See https://github.com/matrix-org/matrix-wysiwyg/blob/main/platforms/web/test.setup.ts
jest.mock("@matrix-org/matrix-wysiwyg", () => ({
useWysiwyg: ({ onChange }) => {
return { ref: { current: null }, isWysiwygReady: true, wysiwyg: { clear: () => void 0 } };
},
}));
describe("MessageComposer", () => {
stubClient();
@ -346,6 +355,16 @@ describe("MessageComposer", () => {
expect(wrapper.find(MessageComposerButtons).props().showStickersButton).toBe(false);
});
});
it('should render WysiwygComposer', () => {
const room = mkStubRoom("!roomId:server", "Room 1", cli);
SettingsStore.setValue("feature_wysiwyg_composer", null, SettingLevel.DEVICE, true);
const wrapper = wrapAndRender({ room });
SettingsStore.setValue("feature_wysiwyg_composer", null, SettingLevel.DEVICE, false);
expect(wrapper.find(WysiwygComposer)).toBeTruthy();
});
});
function wrapAndRender(

View file

@ -0,0 +1,148 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { act, render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import { IRoomState } from "../../../../../src/components/structures/RoomView";
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { Layout } from "../../../../../src/settings/enums/Layout";
import { createTestClient, mkEvent, mkStubRoom } from "../../../../test-utils";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import { WysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer/WysiwygComposer";
let callOnChange: (content: string) => void;
// The wysiwyg fetch wasm bytes and a specific workaround is needed to make it works in a node (jest) environnement
// See https://github.com/matrix-org/matrix-wysiwyg/blob/main/platforms/web/test.setup.ts
jest.mock("@matrix-org/matrix-wysiwyg", () => ({
useWysiwyg: ({ onChange }) => {
callOnChange = onChange;
return { ref: { current: null }, isWysiwygReady: true, wysiwyg: { clear: () => void 0 } };
},
}));
describe('WysiwygComposer', () => {
afterEach(() => {
jest.resetAllMocks();
});
const permalinkCreator = jest.fn() as any;
const mockClient = createTestClient();
const mockEvent = mkEvent({
type: "m.room.message",
room: 'myfakeroom',
user: 'myfakeuser',
content: { "msgtype": "m.text", "body": "Replying to this" },
event: true,
});
const mockRoom = mkStubRoom('myfakeroom', 'myfakeroom', mockClient) as any;
mockRoom.findEventById = jest.fn(eventId => {
return eventId === mockEvent.getId() ? mockEvent : null;
});
const defaultRoomContext: IRoomState = {
room: mockRoom,
roomLoading: true,
peekLoading: false,
shouldPeek: true,
membersLoaded: false,
numUnreadMessages: 0,
canPeek: false,
showApps: false,
isPeeking: false,
showRightPanel: true,
joining: false,
atEndOfLiveTimeline: true,
showTopUnreadMessagesBar: false,
statusBarVisible: false,
canReact: false,
canSendMessages: false,
canSendVoiceBroadcasts: false,
layout: Layout.Group,
lowBandwidth: false,
alwaysShowTimestamps: false,
showTwelveHourTimestamps: false,
readMarkerInViewThresholdMs: 3000,
readMarkerOutOfViewThresholdMs: 30000,
showHiddenEvents: false,
showReadReceipts: true,
showRedactions: true,
showJoinLeaves: true,
showAvatarChanges: true,
showDisplaynameChanges: true,
matrixClientIsReady: false,
timelineRenderingType: TimelineRenderingType.Room,
liveTimeline: undefined,
canSelfRedact: false,
resizing: false,
narrow: false,
activeCall: null,
};
let sendMessage: () => void;
const customRender = (onChange = (content: string) => void 0, disabled = false) => {
return render(
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={defaultRoomContext}>
<WysiwygComposer onChange={onChange} permalinkCreator={permalinkCreator} disabled={disabled}>
{ (_sendMessage) => {
sendMessage = _sendMessage;
} }</WysiwygComposer>
</RoomContext.Provider>
</MatrixClientContext.Provider>,
);
};
it('Should have contentEditable at false when disabled', () => {
// When
customRender(null, true);
// Then
expect(screen.getByRole('textbox')).toHaveAttribute('contentEditable', "false");
});
it('Should call onChange handler', (done) => {
const html = '<b>html</b>';
customRender((content) => {
expect(content).toBe((html));
done();
});
act(() => callOnChange(html));
});
it('Should send message, call clear and focus the textbox', async () => {
// When
const html = '<b>html</b>';
await new Promise((resolve) => {
customRender(() => resolve(null));
act(() => callOnChange(html));
});
act(() => sendMessage());
// Then
const expectedContent = {
"body": html,
"format": "org.matrix.custom.html",
"formatted_body": html,
"msgtype": "m.text",
};
expect(mockClient.sendMessage).toBeCalledWith('myfakeroom', null, expectedContent);
expect(screen.getByRole('textbox')).toHaveFocus();
});
});

View file

@ -0,0 +1,149 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IRoomState } from "../../../../../src/components/structures/RoomView";
import { createMessageContent, sendMessage } from "../../../../../src/components/views/rooms/wysiwyg_composer/message";
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { Layout } from "../../../../../src/settings/enums/Layout";
import { createTestClient, mkEvent, mkStubRoom } from "../../../../test-utils";
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
describe('message', () => {
const permalinkCreator = jest.fn() as any;
const message = '<i><b>hello</b> world</i>';
const mockEvent = mkEvent({
type: "m.room.message",
room: 'myfakeroom',
user: 'myfakeuser',
content: { "msgtype": "m.text", "body": "Replying to this" },
event: true,
});
afterEach(() => {
jest.resetAllMocks();
});
describe('createMessageContent', () => {
it("Should create html message", () => {
// When
const content = createMessageContent(message, { permalinkCreator });
// Then
expect(content).toEqual({
body: message,
format: "org.matrix.custom.html",
formatted_body: message,
msgtype: "m.text",
});
});
});
describe('sendMessage', () => {
const mockClient = createTestClient();
const mockRoom = mkStubRoom('myfakeroom', 'myfakeroom', mockClient) as any;
mockRoom.findEventById = jest.fn(eventId => {
return eventId === mockEvent.getId() ? mockEvent : null;
});
const defaultRoomContext: IRoomState = {
room: mockRoom,
roomLoading: true,
peekLoading: false,
shouldPeek: true,
membersLoaded: false,
numUnreadMessages: 0,
canPeek: false,
showApps: false,
isPeeking: false,
showRightPanel: true,
joining: false,
atEndOfLiveTimeline: true,
showTopUnreadMessagesBar: false,
statusBarVisible: false,
canReact: false,
canSendMessages: false,
canSendVoiceBroadcasts: false,
layout: Layout.Group,
lowBandwidth: false,
alwaysShowTimestamps: false,
showTwelveHourTimestamps: false,
readMarkerInViewThresholdMs: 3000,
readMarkerOutOfViewThresholdMs: 30000,
showHiddenEvents: false,
showReadReceipts: true,
showRedactions: true,
showJoinLeaves: true,
showAvatarChanges: true,
showDisplaynameChanges: true,
matrixClientIsReady: false,
timelineRenderingType: TimelineRenderingType.Room,
liveTimeline: undefined,
canSelfRedact: false,
resizing: false,
narrow: false,
activeCall: null,
};
const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch");
it('Should not send empty html message', async () => {
// When
await sendMessage(message, { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator });
// Then
const expectedContent = {
"body": "<i><b>hello</b> world</i>",
"format": "org.matrix.custom.html",
"formatted_body": "<i><b>hello</b> world</i>",
"msgtype": "m.text",
};
expect(mockClient.sendMessage).toBeCalledWith('myfakeroom', null, expectedContent);
expect(spyDispatcher).toBeCalledWith({ action: 'message_sent' });
});
it('Should send html message', async () => {
// When
await sendMessage('', { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator });
// Then
expect(mockClient.sendMessage).toBeCalledTimes(0);
expect(spyDispatcher).toBeCalledTimes(0);
});
it('Should scroll to bottom after sending a html message', async () => {
// When
SettingsStore.setValue("scrollToBottomOnMessageSent", null, SettingLevel.DEVICE, true);
await sendMessage(message, { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator });
// Then
expect(spyDispatcher).toBeCalledWith(
{ action: 'scroll_to_bottom', timelineRenderingType: defaultRoomContext.timelineRenderingType },
);
});
it('Should handle emojis', async () => {
// When
await sendMessage('🎉', { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator });
// Then
expect(spyDispatcher).toBeCalledWith(
{ action: 'effects.confetti' },
);
});
});
});

View file

@ -1549,6 +1549,11 @@
resolved "https://registry.yarnpkg.com/@matrix-org/analytics-events/-/analytics-events-0.2.0.tgz#453925c939ecdd5ca6c797d293deb8cf0933f1b8"
integrity sha512-+0/Sydm4MNOcqd8iySJmojVPB74Axba4BXlwTsiKmL5fgYqdUkwmqkO39K7Pn8i+a+8pg11oNvBPkpWs3O5Qww==
"@matrix-org/matrix-wysiwyg@^0.0.2":
version "0.0.2"
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-0.0.2.tgz#c1a18f5f9ac061c4147a0fbbf9303a3c82e626e6"
integrity sha512-AY4sbmgcaFZhNxJfn3Va1SiKH4/gIdvWV9c/iehcIi3/xFB7lKCIwe7NNxzPpFOp+b+fEIbdHf3fhS5vJBi7xg==
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz":
version "3.2.8"
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz#8d53636d045e1776e2a2ec6613e57330dd9ce856"