mirror of https://github.com/vector-im/riot-web
Merge pull request #9374 from matrix-org/feat/matrix-wysisyg-integration
First step of matrix-wysiwyg integrationpull/28217/head
commit
b336e18eae
|
@ -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",
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -25,6 +25,10 @@ export interface MatrixClientProps {
|
|||
mxClient: MatrixClient;
|
||||
}
|
||||
|
||||
export function useMatrixClientContext() {
|
||||
return useContext(MatrixClientContext);
|
||||
}
|
||||
|
||||
const matrixHOC = <ComposedComponentProps extends {}>(
|
||||
ComposedComponent: ComponentClass<ComposedComponentProps>,
|
||||
) => {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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' },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue