Improve switching between rich and plain editing modes (#9776)
* allows switching between modes that retains formatting * updates rich text composer dependency to 0.13.0 (@matrix-org/matrix-wysiwyg) * improves handling of enter keypresses when ctrlEnterTosend setting is true in plain text editor * changes the message event content when using the new editor * adds tests for the changes to the plain text editor
This commit is contained in:
parent
3bcea5fb0b
commit
432ce3ca31
13 changed files with 336 additions and 94 deletions
|
@ -57,7 +57,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@matrix-org/analytics-events": "^0.3.0",
|
"@matrix-org/analytics-events": "^0.3.0",
|
||||||
"@matrix-org/matrix-wysiwyg": "^0.11.0",
|
"@matrix-org/matrix-wysiwyg": "^0.13.0",
|
||||||
"@matrix-org/react-sdk-module-api": "^0.0.3",
|
"@matrix-org/react-sdk-module-api": "^0.0.3",
|
||||||
"@sentry/browser": "^7.0.0",
|
"@sentry/browser": "^7.0.0",
|
||||||
"@sentry/tracing": "^7.0.0",
|
"@sentry/tracing": "^7.0.0",
|
||||||
|
|
|
@ -54,9 +54,8 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||||
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
|
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
|
||||||
import { Features } from "../../../settings/Settings";
|
import { Features } from "../../../settings/Settings";
|
||||||
import { VoiceMessageRecording } from "../../../audio/VoiceMessageRecording";
|
import { VoiceMessageRecording } from "../../../audio/VoiceMessageRecording";
|
||||||
import { SendWysiwygComposer, sendMessage } from "./wysiwyg_composer/";
|
import { SendWysiwygComposer, sendMessage, getConversionFunctions } from "./wysiwyg_composer/";
|
||||||
import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext";
|
import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext";
|
||||||
import { htmlToPlainText } from "../../../utils/room/htmlToPlaintext";
|
|
||||||
import { setUpVoiceBroadcastPreRecording } from "../../../voice-broadcast/utils/setUpVoiceBroadcastPreRecording";
|
import { setUpVoiceBroadcastPreRecording } from "../../../voice-broadcast/utils/setUpVoiceBroadcastPreRecording";
|
||||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||||
|
|
||||||
|
@ -333,7 +332,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
if (this.state.isWysiwygLabEnabled) {
|
if (this.state.isWysiwygLabEnabled) {
|
||||||
const { permalinkCreator, relation, replyToEvent } = this.props;
|
const { permalinkCreator, relation, replyToEvent } = this.props;
|
||||||
sendMessage(this.state.composerContent, this.state.isRichTextEnabled, {
|
await sendMessage(this.state.composerContent, this.state.isRichTextEnabled, {
|
||||||
mxClient: this.props.mxClient,
|
mxClient: this.props.mxClient,
|
||||||
roomContext: this.context,
|
roomContext: this.context,
|
||||||
permalinkCreator,
|
permalinkCreator,
|
||||||
|
@ -358,14 +357,19 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRichTextToggle = () => {
|
private onRichTextToggle = async () => {
|
||||||
this.setState((state) => ({
|
const { richToPlain, plainToRich } = await getConversionFunctions();
|
||||||
isRichTextEnabled: !state.isRichTextEnabled,
|
|
||||||
initialComposerContent: !state.isRichTextEnabled
|
const { isRichTextEnabled, composerContent } = this.state;
|
||||||
? state.composerContent
|
const convertedContent = isRichTextEnabled
|
||||||
: // TODO when available use rust model plain text
|
? await richToPlain(composerContent)
|
||||||
htmlToPlainText(state.composerContent),
|
: await plainToRich(composerContent);
|
||||||
}));
|
|
||||||
|
this.setState({
|
||||||
|
isRichTextEnabled: !isRichTextEnabled,
|
||||||
|
composerContent: convertedContent,
|
||||||
|
initialComposerContent: convertedContent,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onVoiceStoreUpdate = () => {
|
private onVoiceStoreUpdate = () => {
|
||||||
|
|
|
@ -16,9 +16,25 @@ limitations under the License.
|
||||||
|
|
||||||
import React, { ComponentProps, lazy, Suspense } from "react";
|
import React, { ComponentProps, lazy, Suspense } from "react";
|
||||||
|
|
||||||
|
// we need to import the types for TS, but do not import the sendMessage
|
||||||
|
// function to avoid importing from "@matrix-org/matrix-wysiwyg"
|
||||||
|
import { SendMessageParams } from "./utils/message";
|
||||||
|
|
||||||
const SendComposer = lazy(() => import("./SendWysiwygComposer"));
|
const SendComposer = lazy(() => import("./SendWysiwygComposer"));
|
||||||
const EditComposer = lazy(() => import("./EditWysiwygComposer"));
|
const EditComposer = lazy(() => import("./EditWysiwygComposer"));
|
||||||
|
|
||||||
|
export const dynamicImportSendMessage = async (message: string, isHTML: boolean, params: SendMessageParams) => {
|
||||||
|
const { sendMessage } = await import("./utils/message");
|
||||||
|
|
||||||
|
return sendMessage(message, isHTML, params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dynamicImportConversionFunctions = async () => {
|
||||||
|
const { richToPlain, plainToRich } = await import("@matrix-org/matrix-wysiwyg");
|
||||||
|
|
||||||
|
return { richToPlain, plainToRich };
|
||||||
|
};
|
||||||
|
|
||||||
export function DynamicImportSendWysiwygComposer(props: ComponentProps<typeof SendComposer>) {
|
export function DynamicImportSendWysiwygComposer(props: ComponentProps<typeof SendComposer>) {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<div />}>
|
<Suspense fallback={<div />}>
|
||||||
|
|
|
@ -17,11 +17,22 @@ limitations under the License.
|
||||||
import { KeyboardEvent, SyntheticEvent, useCallback, useRef, useState } from "react";
|
import { KeyboardEvent, SyntheticEvent, useCallback, useRef, useState } from "react";
|
||||||
|
|
||||||
import { useSettingValue } from "../../../../../hooks/useSettings";
|
import { useSettingValue } from "../../../../../hooks/useSettings";
|
||||||
|
import { IS_MAC, Key } from "../../../../../Keyboard";
|
||||||
|
|
||||||
function isDivElement(target: EventTarget): target is HTMLDivElement {
|
function isDivElement(target: EventTarget): target is HTMLDivElement {
|
||||||
return target instanceof HTMLDivElement;
|
return target instanceof HTMLDivElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hitting enter inside the editor inserts an editable div, initially containing a <br />
|
||||||
|
// For correct display, first replace this pattern with a newline character and then remove divs
|
||||||
|
// noting that they are used to delimit paragraphs
|
||||||
|
function amendInnerHtml(text: string) {
|
||||||
|
return text
|
||||||
|
.replace(/<div><br><\/div>/g, "\n") // this is pressing enter then not typing
|
||||||
|
.replace(/<div>/g, "\n") // this is from pressing enter, then typing inside the div
|
||||||
|
.replace(/<\/div>/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
export function usePlainTextListeners(
|
export function usePlainTextListeners(
|
||||||
initialContent?: string,
|
initialContent?: string,
|
||||||
onChange?: (content: string) => void,
|
onChange?: (content: string) => void,
|
||||||
|
@ -44,25 +55,39 @@ export function usePlainTextListeners(
|
||||||
[onChange],
|
[onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const enterShouldSend = !useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
|
||||||
const onInput = useCallback(
|
const onInput = useCallback(
|
||||||
(event: SyntheticEvent<HTMLDivElement, InputEvent | ClipboardEvent>) => {
|
(event: SyntheticEvent<HTMLDivElement, InputEvent | ClipboardEvent>) => {
|
||||||
if (isDivElement(event.target)) {
|
if (isDivElement(event.target)) {
|
||||||
setText(event.target.innerHTML);
|
// if enterShouldSend, we do not need to amend the html before setting text
|
||||||
|
const newInnerHTML = enterShouldSend ? event.target.innerHTML : amendInnerHtml(event.target.innerHTML);
|
||||||
|
setText(newInnerHTML);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setText],
|
[setText, enterShouldSend],
|
||||||
);
|
);
|
||||||
|
|
||||||
const isCtrlEnter = useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
|
|
||||||
const onKeyDown = useCallback(
|
const onKeyDown = useCallback(
|
||||||
(event: KeyboardEvent<HTMLDivElement>) => {
|
(event: KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (event.key === "Enter" && !event.shiftKey && (!isCtrlEnter || (isCtrlEnter && event.ctrlKey))) {
|
if (event.key === Key.ENTER) {
|
||||||
event.preventDefault();
|
const sendModifierIsPressed = IS_MAC ? event.metaKey : event.ctrlKey;
|
||||||
event.stopPropagation();
|
|
||||||
send();
|
// if enter should send, send if the user is not pushing shift
|
||||||
|
if (enterShouldSend && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
send();
|
||||||
|
}
|
||||||
|
|
||||||
|
// if enter should not send, send only if the user is pushing ctrl/cmd
|
||||||
|
if (!enterShouldSend && sendModifierIsPressed) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
send();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isCtrlEnter, send],
|
[enterShouldSend, send],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { ref, onInput, onPaste: onInput, onKeyDown, content, setContent: setText };
|
return { ref, onInput, onPaste: onInput, onKeyDown, content, setContent: setText };
|
||||||
|
|
|
@ -17,5 +17,6 @@ limitations under the License.
|
||||||
export {
|
export {
|
||||||
DynamicImportSendWysiwygComposer as SendWysiwygComposer,
|
DynamicImportSendWysiwygComposer as SendWysiwygComposer,
|
||||||
DynamicImportEditWysiwygComposer as EditWysiwygComposer,
|
DynamicImportEditWysiwygComposer as EditWysiwygComposer,
|
||||||
|
dynamicImportSendMessage as sendMessage,
|
||||||
|
dynamicImportConversionFunctions as getConversionFunctions,
|
||||||
} from "./DynamicImportWysiwygComposer";
|
} from "./DynamicImportWysiwygComposer";
|
||||||
export { sendMessage } from "./utils/message";
|
|
||||||
|
|
|
@ -14,13 +14,12 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { richToPlain, plainToRich } from "@matrix-org/matrix-wysiwyg";
|
||||||
import { IContent, IEventRelation, MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix";
|
import { IContent, IEventRelation, MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { htmlSerializeFromMdIfNeeded } from "../../../../../editor/serialize";
|
|
||||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
import { RoomPermalinkCreator } from "../../../../../utils/permalinks/Permalinks";
|
import { RoomPermalinkCreator } from "../../../../../utils/permalinks/Permalinks";
|
||||||
import { addReplyToMessageContent } from "../../../../../utils/Reply";
|
import { addReplyToMessageContent } from "../../../../../utils/Reply";
|
||||||
import { htmlToPlainText } from "../../../../../utils/room/htmlToPlaintext";
|
|
||||||
|
|
||||||
// Merges favouring the given relation
|
// Merges favouring the given relation
|
||||||
function attachRelation(content: IContent, relation?: IEventRelation): void {
|
function attachRelation(content: IContent, relation?: IEventRelation): void {
|
||||||
|
@ -62,7 +61,7 @@ interface CreateMessageContentParams {
|
||||||
editedEvent?: MatrixEvent;
|
editedEvent?: MatrixEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMessageContent(
|
export async function createMessageContent(
|
||||||
message: string,
|
message: string,
|
||||||
isHTML: boolean,
|
isHTML: boolean,
|
||||||
{
|
{
|
||||||
|
@ -72,7 +71,7 @@ export function createMessageContent(
|
||||||
includeReplyLegacyFallback = true,
|
includeReplyLegacyFallback = true,
|
||||||
editedEvent,
|
editedEvent,
|
||||||
}: CreateMessageContentParams,
|
}: CreateMessageContentParams,
|
||||||
): IContent {
|
): Promise<IContent> {
|
||||||
// TODO emote ?
|
// TODO emote ?
|
||||||
|
|
||||||
const isEditing = Boolean(editedEvent);
|
const isEditing = Boolean(editedEvent);
|
||||||
|
@ -90,26 +89,22 @@ export function createMessageContent(
|
||||||
|
|
||||||
// const body = textSerialize(model);
|
// const body = textSerialize(model);
|
||||||
|
|
||||||
// TODO remove this ugly hack for replace br tag
|
// if we're editing rich text, the message content is pure html
|
||||||
const body = (isHTML && htmlToPlainText(message)) || message.replace(/<br>/g, "\n");
|
// BUT if we're not, the message content will be plain text
|
||||||
|
const body = isHTML ? await richToPlain(message) : message;
|
||||||
const bodyPrefix = (isReplyAndEditing && getTextReplyFallback(editedEvent)) || "";
|
const bodyPrefix = (isReplyAndEditing && getTextReplyFallback(editedEvent)) || "";
|
||||||
const formattedBodyPrefix = (isReplyAndEditing && getHtmlReplyFallback(editedEvent)) || "";
|
const formattedBodyPrefix = (isReplyAndEditing && getHtmlReplyFallback(editedEvent)) || "";
|
||||||
|
|
||||||
const content: IContent = {
|
const content: IContent = {
|
||||||
// TODO emote
|
// TODO emote
|
||||||
msgtype: MsgType.Text,
|
msgtype: MsgType.Text,
|
||||||
// TODO when available, use HTML --> Plain text conversion from wysiwyg rust model
|
|
||||||
body: isEditing ? `${bodyPrefix} * ${body}` : body,
|
body: isEditing ? `${bodyPrefix} * ${body}` : body,
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO markdown support
|
// TODO markdown support
|
||||||
|
|
||||||
const isMarkdownEnabled = SettingsStore.getValue<boolean>("MessageComposerInput.useMarkdown");
|
const isMarkdownEnabled = SettingsStore.getValue<boolean>("MessageComposerInput.useMarkdown");
|
||||||
const formattedBody = isHTML
|
const formattedBody = isHTML ? message : isMarkdownEnabled ? await plainToRich(message) : null;
|
||||||
? message
|
|
||||||
: isMarkdownEnabled
|
|
||||||
? htmlSerializeFromMdIfNeeded(message, { forceHTML: isReply })
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (formattedBody) {
|
if (formattedBody) {
|
||||||
content.format = "org.matrix.custom.html";
|
content.format = "org.matrix.custom.html";
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Composer as ComposerEvent } from "@matrix-org/analytics-events/types/typescript/Composer";
|
import { Composer as ComposerEvent } from "@matrix-org/analytics-events/types/typescript/Composer";
|
||||||
import { IContent, IEventRelation, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { IEventRelation, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { ISendEventResponse, MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { ISendEventResponse, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
|
||||||
import { createMessageContent } from "./createMessageContent";
|
import { createMessageContent } from "./createMessageContent";
|
||||||
import { isContentModified } from "./isContentModified";
|
import { isContentModified } from "./isContentModified";
|
||||||
|
|
||||||
interface SendMessageParams {
|
export interface SendMessageParams {
|
||||||
mxClient: MatrixClient;
|
mxClient: MatrixClient;
|
||||||
relation?: IEventRelation;
|
relation?: IEventRelation;
|
||||||
replyToEvent?: MatrixEvent;
|
replyToEvent?: MatrixEvent;
|
||||||
|
@ -43,10 +43,18 @@ interface SendMessageParams {
|
||||||
includeReplyLegacyFallback?: boolean;
|
includeReplyLegacyFallback?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendMessage(message: string, isHTML: boolean, { roomContext, mxClient, ...params }: SendMessageParams) {
|
export async function sendMessage(
|
||||||
|
message: string,
|
||||||
|
isHTML: boolean,
|
||||||
|
{ roomContext, mxClient, ...params }: SendMessageParams,
|
||||||
|
) {
|
||||||
const { relation, replyToEvent } = params;
|
const { relation, replyToEvent } = params;
|
||||||
const { room } = roomContext;
|
const { room } = roomContext;
|
||||||
const { roomId } = room;
|
const roomId = room?.roomId;
|
||||||
|
|
||||||
|
if (!roomId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const posthogEvent: ComposerEvent = {
|
const posthogEvent: ComposerEvent = {
|
||||||
eventName: "Composer",
|
eventName: "Composer",
|
||||||
|
@ -63,7 +71,7 @@ export function sendMessage(message: string, isHTML: boolean, { roomContext, mxC
|
||||||
}*/
|
}*/
|
||||||
PosthogAnalytics.instance.trackEvent<ComposerEvent>(posthogEvent);
|
PosthogAnalytics.instance.trackEvent<ComposerEvent>(posthogEvent);
|
||||||
|
|
||||||
let content: IContent;
|
const content = await createMessageContent(message, isHTML, params);
|
||||||
|
|
||||||
// TODO slash comment
|
// TODO slash comment
|
||||||
|
|
||||||
|
@ -71,10 +79,6 @@ export function sendMessage(message: string, isHTML: boolean, { roomContext, mxC
|
||||||
|
|
||||||
// TODO quick reaction
|
// TODO quick reaction
|
||||||
|
|
||||||
if (!content) {
|
|
||||||
content = createMessageContent(message, isHTML, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't bother sending an empty message
|
// don't bother sending an empty message
|
||||||
if (!content.body.trim()) {
|
if (!content.body.trim()) {
|
||||||
return;
|
return;
|
||||||
|
@ -84,7 +88,7 @@ export function sendMessage(message: string, isHTML: boolean, { roomContext, mxC
|
||||||
decorateStartSendingTime(content);
|
decorateStartSendingTime(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation.event_id : null;
|
const threadId = relation?.event_id && relation?.rel_type === THREAD_RELATION_TYPE.name ? relation.event_id : null;
|
||||||
|
|
||||||
const prom = doMaybeLocalRoomAction(
|
const prom = doMaybeLocalRoomAction(
|
||||||
roomId,
|
roomId,
|
||||||
|
@ -139,7 +143,7 @@ interface EditMessageParams {
|
||||||
editorStateTransfer: EditorStateTransfer;
|
editorStateTransfer: EditorStateTransfer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function editMessage(html: string, { roomContext, mxClient, editorStateTransfer }: EditMessageParams) {
|
export async function editMessage(html: string, { roomContext, mxClient, editorStateTransfer }: EditMessageParams) {
|
||||||
const editedEvent = editorStateTransfer.getEvent();
|
const editedEvent = editorStateTransfer.getEvent();
|
||||||
|
|
||||||
PosthogAnalytics.instance.trackEvent<ComposerEvent>({
|
PosthogAnalytics.instance.trackEvent<ComposerEvent>({
|
||||||
|
@ -156,7 +160,7 @@ export function editMessage(html: string, { roomContext, mxClient, editorStateTr
|
||||||
const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
|
const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||||
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
|
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
|
||||||
}*/
|
}*/
|
||||||
const editContent = createMessageContent(html, true, { editedEvent });
|
const editContent = await createMessageContent(html, true, { editedEvent });
|
||||||
const newContent = editContent["m.new_content"];
|
const newContent = editContent["m.new_content"];
|
||||||
|
|
||||||
const shouldSend = true;
|
const shouldSend = true;
|
||||||
|
@ -174,10 +178,10 @@ export function editMessage(html: string, { roomContext, mxClient, editorStateTr
|
||||||
|
|
||||||
let response: Promise<ISendEventResponse> | undefined;
|
let response: Promise<ISendEventResponse> | undefined;
|
||||||
|
|
||||||
// If content is modified then send an updated event into the room
|
const roomId = editedEvent.getRoomId();
|
||||||
if (isContentModified(newContent, editorStateTransfer)) {
|
|
||||||
const roomId = editedEvent.getRoomId();
|
|
||||||
|
|
||||||
|
// If content is modified then send an updated event into the room
|
||||||
|
if (isContentModified(newContent, editorStateTransfer) && roomId) {
|
||||||
// TODO Slash Commands
|
// TODO Slash Commands
|
||||||
|
|
||||||
if (shouldSend) {
|
if (shouldSend) {
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
/*
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function htmlToPlainText(html: string) {
|
|
||||||
return new DOMParser().parseFromString(html, "text/html").documentElement.textContent;
|
|
||||||
}
|
|
|
@ -229,7 +229,10 @@ describe("EditWysiwygComposer", () => {
|
||||||
},
|
},
|
||||||
"msgtype": "m.text",
|
"msgtype": "m.text",
|
||||||
};
|
};
|
||||||
expect(mockClient.sendMessage).toBeCalledWith(mockEvent.getRoomId(), null, expectedContent);
|
await waitFor(() =>
|
||||||
|
expect(mockClient.sendMessage).toBeCalledWith(mockEvent.getRoomId(), null, expectedContent),
|
||||||
|
);
|
||||||
|
|
||||||
expect(spyDispatcher).toBeCalledWith({ action: "message_sent" });
|
expect(spyDispatcher).toBeCalledWith({ action: "message_sent" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,6 +19,8 @@ import { act, render, screen } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
import { PlainTextComposer } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer";
|
import { PlainTextComposer } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer";
|
||||||
|
import * as mockUseSettingsHook from "../../../../../../src/hooks/useSettings";
|
||||||
|
import * as mockKeyboard from "../../../../../../src/Keyboard";
|
||||||
|
|
||||||
describe("PlainTextComposer", () => {
|
describe("PlainTextComposer", () => {
|
||||||
const customRender = (
|
const customRender = (
|
||||||
|
@ -37,6 +39,17 @@ describe("PlainTextComposer", () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mockUseSettingValue: jest.SpyInstance;
|
||||||
|
beforeEach(() => {
|
||||||
|
// defaults for these tests are:
|
||||||
|
// ctrlEnterToSend is false
|
||||||
|
mockUseSettingValue = jest.spyOn(mockUseSettingsHook, "useSettingValue").mockReturnValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it("Should have contentEditable at false when disabled", () => {
|
it("Should have contentEditable at false when disabled", () => {
|
||||||
// When
|
// When
|
||||||
customRender(jest.fn(), jest.fn(), true);
|
customRender(jest.fn(), jest.fn(), true);
|
||||||
|
@ -64,7 +77,7 @@ describe("PlainTextComposer", () => {
|
||||||
expect(onChange).toBeCalledWith(content);
|
expect(onChange).toBeCalledWith(content);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should call onSend when Enter is pressed", async () => {
|
it("Should call onSend when Enter is pressed when ctrlEnterToSend is false", async () => {
|
||||||
//When
|
//When
|
||||||
const onSend = jest.fn();
|
const onSend = jest.fn();
|
||||||
customRender(jest.fn(), onSend);
|
customRender(jest.fn(), onSend);
|
||||||
|
@ -74,9 +87,134 @@ describe("PlainTextComposer", () => {
|
||||||
expect(onSend).toBeCalledTimes(1);
|
expect(onSend).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Should not call onSend when Enter is pressed when ctrlEnterToSend is true", async () => {
|
||||||
|
//When
|
||||||
|
mockUseSettingValue.mockReturnValue(true);
|
||||||
|
const onSend = jest.fn();
|
||||||
|
customRender(jest.fn(), onSend);
|
||||||
|
await userEvent.type(screen.getByRole("textbox"), "{enter}");
|
||||||
|
|
||||||
|
// Then it does not send a message
|
||||||
|
expect(onSend).toBeCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should only call onSend when ctrl+enter is pressed when ctrlEnterToSend is true on windows", async () => {
|
||||||
|
//When
|
||||||
|
mockUseSettingValue.mockReturnValue(true);
|
||||||
|
|
||||||
|
const onSend = jest.fn();
|
||||||
|
customRender(jest.fn(), onSend);
|
||||||
|
const textBox = screen.getByRole("textbox");
|
||||||
|
await userEvent.type(textBox, "hello");
|
||||||
|
|
||||||
|
// Then it does NOT send a message on enter
|
||||||
|
await userEvent.type(textBox, "{enter}");
|
||||||
|
expect(onSend).toBeCalledTimes(0);
|
||||||
|
|
||||||
|
// Then it does NOT send a message on windows+enter
|
||||||
|
await userEvent.type(textBox, "{meta>}{enter}{meta/}");
|
||||||
|
expect(onSend).toBeCalledTimes(0);
|
||||||
|
|
||||||
|
// Then it does send a message on ctrl+enter
|
||||||
|
await userEvent.type(textBox, "{control>}{enter}{control/}");
|
||||||
|
expect(onSend).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should only call onSend when cmd+enter is pressed when ctrlEnterToSend is true on mac", async () => {
|
||||||
|
//When
|
||||||
|
mockUseSettingValue.mockReturnValue(true);
|
||||||
|
Object.defineProperty(mockKeyboard, "IS_MAC", { value: true });
|
||||||
|
|
||||||
|
const onSend = jest.fn();
|
||||||
|
customRender(jest.fn(), onSend);
|
||||||
|
const textBox = screen.getByRole("textbox");
|
||||||
|
await userEvent.type(textBox, "hello");
|
||||||
|
|
||||||
|
// Then it does NOT send a message on enter
|
||||||
|
await userEvent.type(textBox, "{enter}");
|
||||||
|
expect(onSend).toBeCalledTimes(0);
|
||||||
|
|
||||||
|
// Then it does NOT send a message on ctrl+enter
|
||||||
|
await userEvent.type(textBox, "{control>}{enter}{control/}");
|
||||||
|
expect(onSend).toBeCalledTimes(0);
|
||||||
|
|
||||||
|
// Then it does send a message on cmd+enter
|
||||||
|
await userEvent.type(textBox, "{meta>}{enter}{meta/}");
|
||||||
|
expect(onSend).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should insert a newline character when shift enter is pressed when ctrlEnterToSend is false", async () => {
|
||||||
|
//When
|
||||||
|
const onSend = jest.fn();
|
||||||
|
customRender(jest.fn(), onSend);
|
||||||
|
const textBox = screen.getByRole("textbox");
|
||||||
|
const inputWithShiftEnter = "new{Shift>}{enter}{/Shift}line";
|
||||||
|
const expectedInnerHtml = "new\nline";
|
||||||
|
|
||||||
|
await userEvent.click(textBox);
|
||||||
|
await userEvent.type(textBox, inputWithShiftEnter);
|
||||||
|
|
||||||
|
// Then it does not send a message, but inserts a newline character
|
||||||
|
expect(onSend).toBeCalledTimes(0);
|
||||||
|
expect(textBox.innerHTML).toBe(expectedInnerHtml);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should insert a newline character when shift enter is pressed when ctrlEnterToSend is true", async () => {
|
||||||
|
//When
|
||||||
|
mockUseSettingValue.mockReturnValue(true);
|
||||||
|
const onSend = jest.fn();
|
||||||
|
customRender(jest.fn(), onSend);
|
||||||
|
const textBox = screen.getByRole("textbox");
|
||||||
|
const keyboardInput = "new{Shift>}{enter}{/Shift}line";
|
||||||
|
const expectedInnerHtml = "new\nline";
|
||||||
|
|
||||||
|
await userEvent.click(textBox);
|
||||||
|
await userEvent.type(textBox, keyboardInput);
|
||||||
|
|
||||||
|
// Then it does not send a message, but inserts a newline character
|
||||||
|
expect(onSend).toBeCalledTimes(0);
|
||||||
|
expect(textBox.innerHTML).toBe(expectedInnerHtml);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should not insert div and br tags when enter is pressed when ctrlEnterToSend is true", async () => {
|
||||||
|
//When
|
||||||
|
mockUseSettingValue.mockReturnValue(true);
|
||||||
|
const onSend = jest.fn();
|
||||||
|
customRender(jest.fn(), onSend);
|
||||||
|
const textBox = screen.getByRole("textbox");
|
||||||
|
const enterThenTypeHtml = "<div>hello</div";
|
||||||
|
|
||||||
|
await userEvent.click(textBox);
|
||||||
|
await userEvent.type(textBox, "{enter}hello");
|
||||||
|
|
||||||
|
// Then it does not send a message, but inserts a newline character
|
||||||
|
expect(onSend).toBeCalledTimes(0);
|
||||||
|
expect(textBox).not.toContainHTML(enterThenTypeHtml);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should not insert div tags when enter is pressed then user types more when ctrlEnterToSend is true", async () => {
|
||||||
|
//When
|
||||||
|
mockUseSettingValue.mockReturnValue(true);
|
||||||
|
const onSend = jest.fn();
|
||||||
|
customRender(jest.fn(), onSend);
|
||||||
|
const textBox = screen.getByRole("textbox");
|
||||||
|
const defaultEnterHtml = "<div><br></div";
|
||||||
|
|
||||||
|
await userEvent.click(textBox);
|
||||||
|
await userEvent.type(textBox, "{enter}");
|
||||||
|
|
||||||
|
// Then it does not send a message, but inserts a newline character
|
||||||
|
expect(onSend).toBeCalledTimes(0);
|
||||||
|
expect(textBox).not.toContainHTML(defaultEnterHtml);
|
||||||
|
});
|
||||||
|
|
||||||
it("Should clear textbox content when clear is called", async () => {
|
it("Should clear textbox content when clear is called", async () => {
|
||||||
//When
|
//When
|
||||||
let composer;
|
let composer: {
|
||||||
|
clear: () => void;
|
||||||
|
insertText: (text: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<PlainTextComposer onChange={jest.fn()} onSend={jest.fn()}>
|
<PlainTextComposer onChange={jest.fn()} onSend={jest.fn()}>
|
||||||
{(ref, composerFunctions) => {
|
{(ref, composerFunctions) => {
|
||||||
|
@ -85,9 +223,11 @@ describe("PlainTextComposer", () => {
|
||||||
}}
|
}}
|
||||||
</PlainTextComposer>,
|
</PlainTextComposer>,
|
||||||
);
|
);
|
||||||
|
|
||||||
await userEvent.type(screen.getByRole("textbox"), "content");
|
await userEvent.type(screen.getByRole("textbox"), "content");
|
||||||
expect(screen.getByRole("textbox").innerHTML).toBe("content");
|
expect(screen.getByRole("textbox").innerHTML).toBe("content");
|
||||||
composer.clear();
|
|
||||||
|
composer!.clear();
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(screen.getByRole("textbox").innerHTML).toBeFalsy();
|
expect(screen.getByRole("textbox").innerHTML).toBeFalsy();
|
||||||
|
@ -112,7 +252,7 @@ describe("PlainTextComposer", () => {
|
||||||
render(<PlainTextComposer onChange={jest.fn()} onSend={jest.fn()} />);
|
render(<PlainTextComposer onChange={jest.fn()} onSend={jest.fn()} />);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(screen.getByTestId("WysiwygComposerEditor").attributes["data-is-expanded"].value).toBe("false");
|
expect(screen.getByTestId("WysiwygComposerEditor").dataset["isExpanded"]).toBe("false");
|
||||||
expect(editor).toBe(screen.getByRole("textbox"));
|
expect(editor).toBe(screen.getByRole("textbox"));
|
||||||
|
|
||||||
// When
|
// When
|
||||||
|
@ -126,7 +266,7 @@ describe("PlainTextComposer", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(screen.getByTestId("WysiwygComposerEditor").attributes["data-is-expanded"].value).toBe("true");
|
expect(screen.getByTestId("WysiwygComposerEditor").dataset["isExpanded"]).toBe("true");
|
||||||
|
|
||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
(global.ResizeObserver as jest.Mock).mockRestore();
|
(global.ResizeObserver as jest.Mock).mockRestore();
|
||||||
|
|
|
@ -24,7 +24,7 @@ describe("createMessageContent", () => {
|
||||||
return "$$permalink$$";
|
return "$$permalink$$";
|
||||||
},
|
},
|
||||||
} as RoomPermalinkCreator;
|
} as RoomPermalinkCreator;
|
||||||
const message = "<i><b>hello</b> world</i>";
|
const message = "<em><b>hello</b> world</em>";
|
||||||
const mockEvent = mkEvent({
|
const mockEvent = mkEvent({
|
||||||
type: "m.room.message",
|
type: "m.room.message",
|
||||||
room: "myfakeroom",
|
room: "myfakeroom",
|
||||||
|
@ -37,31 +37,31 @@ describe("createMessageContent", () => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should create html message", () => {
|
it("Should create html message", async () => {
|
||||||
// When
|
// When
|
||||||
const content = createMessageContent(message, true, { permalinkCreator });
|
const content = await createMessageContent(message, true, { permalinkCreator });
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(content).toEqual({
|
expect(content).toEqual({
|
||||||
body: "hello world",
|
body: "*__hello__ world*",
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
formatted_body: message,
|
formatted_body: message,
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should add reply to message content", () => {
|
it("Should add reply to message content", async () => {
|
||||||
// When
|
// When
|
||||||
const content = createMessageContent(message, true, { permalinkCreator, replyToEvent: mockEvent });
|
const content = await createMessageContent(message, true, { permalinkCreator, replyToEvent: mockEvent });
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(content).toEqual({
|
expect(content).toEqual({
|
||||||
"body": "> <myfakeuser> Replying to this\n\nhello world",
|
"body": "> <myfakeuser> Replying to this\n\n*__hello__ world*",
|
||||||
"format": "org.matrix.custom.html",
|
"format": "org.matrix.custom.html",
|
||||||
"formatted_body":
|
"formatted_body":
|
||||||
'<mx-reply><blockquote><a href="$$permalink$$">In reply to</a>' +
|
'<mx-reply><blockquote><a href="$$permalink$$">In reply to</a>' +
|
||||||
' <a href="https://matrix.to/#/myfakeuser">myfakeuser</a>' +
|
' <a href="https://matrix.to/#/myfakeuser">myfakeuser</a>' +
|
||||||
"<br>Replying to this</blockquote></mx-reply><i><b>hello</b> world</i>",
|
"<br>Replying to this</blockquote></mx-reply><em><b>hello</b> world</em>",
|
||||||
"msgtype": "m.text",
|
"msgtype": "m.text",
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
"m.in_reply_to": {
|
"m.in_reply_to": {
|
||||||
|
@ -71,17 +71,17 @@ describe("createMessageContent", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should add relation to message", () => {
|
it("Should add relation to message", async () => {
|
||||||
// When
|
// When
|
||||||
const relation = {
|
const relation = {
|
||||||
rel_type: "m.thread",
|
rel_type: "m.thread",
|
||||||
event_id: "myFakeThreadId",
|
event_id: "myFakeThreadId",
|
||||||
};
|
};
|
||||||
const content = createMessageContent(message, true, { permalinkCreator, relation });
|
const content = await createMessageContent(message, true, { permalinkCreator, relation });
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(content).toEqual({
|
expect(content).toEqual({
|
||||||
"body": "hello world",
|
"body": "*__hello__ world*",
|
||||||
"format": "org.matrix.custom.html",
|
"format": "org.matrix.custom.html",
|
||||||
"formatted_body": message,
|
"formatted_body": message,
|
||||||
"msgtype": "m.text",
|
"msgtype": "m.text",
|
||||||
|
@ -92,7 +92,7 @@ describe("createMessageContent", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should add fields related to edition", () => {
|
it("Should add fields related to edition", async () => {
|
||||||
// When
|
// When
|
||||||
const editedEvent = mkEvent({
|
const editedEvent = mkEvent({
|
||||||
type: "m.room.message",
|
type: "m.room.message",
|
||||||
|
@ -110,16 +110,16 @@ describe("createMessageContent", () => {
|
||||||
},
|
},
|
||||||
event: true,
|
event: true,
|
||||||
});
|
});
|
||||||
const content = createMessageContent(message, true, { permalinkCreator, editedEvent });
|
const content = await createMessageContent(message, true, { permalinkCreator, editedEvent });
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(content).toEqual({
|
expect(content).toEqual({
|
||||||
"body": " * hello world",
|
"body": " * *__hello__ world*",
|
||||||
"format": "org.matrix.custom.html",
|
"format": "org.matrix.custom.html",
|
||||||
"formatted_body": ` * ${message}`,
|
"formatted_body": ` * ${message}`,
|
||||||
"msgtype": "m.text",
|
"msgtype": "m.text",
|
||||||
"m.new_content": {
|
"m.new_content": {
|
||||||
body: "hello world",
|
body: "*__hello__ world*",
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
formatted_body: message,
|
formatted_body: message,
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
|
|
|
@ -70,6 +70,79 @@ describe("message", () => {
|
||||||
expect(spyDispatcher).toBeCalledTimes(0);
|
expect(spyDispatcher).toBeCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Should not send message when there is no roomId", async () => {
|
||||||
|
// When
|
||||||
|
const mockRoomWithoutId = mkStubRoom("", "room without id", mockClient) as any;
|
||||||
|
const mockRoomContextWithoutId: IRoomState = getRoomContext(mockRoomWithoutId, {});
|
||||||
|
|
||||||
|
await sendMessage(message, true, {
|
||||||
|
roomContext: mockRoomContextWithoutId,
|
||||||
|
mxClient: mockClient,
|
||||||
|
permalinkCreator,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockClient.sendMessage).toBeCalledTimes(0);
|
||||||
|
expect(spyDispatcher).toBeCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("calls client.sendMessage with", () => {
|
||||||
|
it("a null argument if SendMessageParams is missing relation", async () => {
|
||||||
|
// When
|
||||||
|
await sendMessage(message, true, {
|
||||||
|
roomContext: defaultRoomContext,
|
||||||
|
mxClient: mockClient,
|
||||||
|
permalinkCreator,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockClient.sendMessage).toHaveBeenCalledWith(expect.anything(), null, expect.anything());
|
||||||
|
});
|
||||||
|
it("a null argument if SendMessageParams has relation but relation is missing event_id", async () => {
|
||||||
|
// When
|
||||||
|
await sendMessage(message, true, {
|
||||||
|
roomContext: defaultRoomContext,
|
||||||
|
mxClient: mockClient,
|
||||||
|
permalinkCreator,
|
||||||
|
relation: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockClient.sendMessage).toBeCalledWith(expect.anything(), null, expect.anything());
|
||||||
|
});
|
||||||
|
it("a null argument if SendMessageParams has relation but rel_type does not match THREAD_RELATION_TYPE.name", async () => {
|
||||||
|
// When
|
||||||
|
await sendMessage(message, true, {
|
||||||
|
roomContext: defaultRoomContext,
|
||||||
|
mxClient: mockClient,
|
||||||
|
permalinkCreator,
|
||||||
|
relation: {
|
||||||
|
event_id: "valid_id",
|
||||||
|
rel_type: "m.does_not_match",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockClient.sendMessage).toBeCalledWith(expect.anything(), null, expect.anything());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the event_id if SendMessageParams has relation and rel_type matches THREAD_RELATION_TYPE.name", async () => {
|
||||||
|
// When
|
||||||
|
await sendMessage(message, true, {
|
||||||
|
roomContext: defaultRoomContext,
|
||||||
|
mxClient: mockClient,
|
||||||
|
permalinkCreator,
|
||||||
|
relation: {
|
||||||
|
event_id: "valid_id",
|
||||||
|
rel_type: "m.thread",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockClient.sendMessage).toBeCalledWith(expect.anything(), "valid_id", expect.anything());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("Should send html message", async () => {
|
it("Should send html message", async () => {
|
||||||
// When
|
// When
|
||||||
await sendMessage(message, true, {
|
await sendMessage(message, true, {
|
||||||
|
@ -80,7 +153,7 @@ describe("message", () => {
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
const expectedContent = {
|
const expectedContent = {
|
||||||
body: "hello world",
|
body: "*__hello__ world*",
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
formatted_body: "<i><b>hello</b> world</i>",
|
formatted_body: "<i><b>hello</b> world</i>",
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
|
@ -114,7 +187,7 @@ describe("message", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const expectedContent = {
|
const expectedContent = {
|
||||||
"body": "> <myfakeuser2> My reply\n\nhello world",
|
"body": "> <myfakeuser2> My reply\n\n*__hello__ world*",
|
||||||
"format": "org.matrix.custom.html",
|
"format": "org.matrix.custom.html",
|
||||||
"formatted_body":
|
"formatted_body":
|
||||||
'<mx-reply><blockquote><a href="$$permalink$$">In reply to</a>' +
|
'<mx-reply><blockquote><a href="$$permalink$$">In reply to</a>' +
|
||||||
|
|
|
@ -1525,10 +1525,10 @@
|
||||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.2.tgz#a09d0fea858e817da971a3c9f904632ef7b49eb6"
|
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.2.tgz#a09d0fea858e817da971a3c9f904632ef7b49eb6"
|
||||||
integrity sha512-oVkBCh9YP7H9i4gAoQbZzswniczfo/aIptNa4dxRi4Ff9lSvUCFv6Hvzi7C+90c0/PWZLXjIDTIAWZYmwyd2fA==
|
integrity sha512-oVkBCh9YP7H9i4gAoQbZzswniczfo/aIptNa4dxRi4Ff9lSvUCFv6Hvzi7C+90c0/PWZLXjIDTIAWZYmwyd2fA==
|
||||||
|
|
||||||
"@matrix-org/matrix-wysiwyg@^0.11.0":
|
"@matrix-org/matrix-wysiwyg@^0.13.0":
|
||||||
version "0.11.0"
|
version "0.13.0"
|
||||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-0.11.0.tgz#3000ee809a3e38242c5da47bef17c572582f2f6b"
|
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-0.13.0.tgz#e643df4e13cdc5dbf9285740bc0ce2aef9873c16"
|
||||||
integrity sha512-B16iLfNnW4PKG4fpDuwJVc0QUrUUqTkhwJ/kxzawcxwVNmWbsPCWJ3hkextYrN2gqRL1d4CNASkNbWLCNNiXhA==
|
integrity sha512-MCeTj4hkl0snjlygd1v+mEEOgaN6agyjAVjJEbvEvP/BaYaDiPEXMTDaRQrcUt3OIY53UNhm1DDEn4yPTn83Jg==
|
||||||
|
|
||||||
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz":
|
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz":
|
||||||
version "3.2.14"
|
version "3.2.14"
|
||||||
|
|
Loading…
Reference in a new issue