mirror of
https://github.com/element-hq/element-web
synced 2024-11-24 02:05:45 +03:00
Merge pull request #9503 from matrix-org/feat/add-plain-text-mode
Feat/add plain text mode
This commit is contained in:
commit
0173e21129
26 changed files with 636 additions and 156 deletions
|
@ -240,7 +240,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
.mx_MessageComposer_wysiwyg {
|
.mx_MessageComposer_wysiwyg {
|
||||||
.mx_MessageComposer_e2eIcon.mx_E2EIcon,.mx_MessageComposer_button, .mx_MessageComposer_sendMessage {
|
.mx_MessageComposer_e2eIcon.mx_E2EIcon,.mx_MessageComposer_button, .mx_MessageComposer_sendMessage {
|
||||||
margin-top: 22px;
|
margin-top: 28px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,6 +264,14 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg');
|
mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_MessageComposer_plain_text::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/composer/plain_text.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageComposer_rich_text::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/composer/rich_text.svg');
|
||||||
|
}
|
||||||
|
|
||||||
.mx_MessageComposer_location::before {
|
.mx_MessageComposer_location::before {
|
||||||
mask-image: url('$(res)/img/element-icons/room/composer/location.svg');
|
mask-image: url('$(res)/img/element-icons/room/composer/location.svg');
|
||||||
}
|
}
|
||||||
|
|
10
res/img/element-icons/room/composer/plain_text.svg
Normal file
10
res/img/element-icons/room/composer/plain_text.svg
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_1456_146350)">
|
||||||
|
<path d="M1 18.6667C1 19.4 1.6 20 2.33333 20H18.3333C19.0667 20 19.6667 19.4 19.6667 18.6667C19.6667 17.9333 19.0667 17.3333 18.3333 17.3333H2.33333C1.6 17.3333 1 17.9333 1 18.6667ZM7 11.7333H13.6667L14.5467 13.8667C14.7467 14.3467 15.2133 14.6667 15.7333 14.6667C16.6533 14.6667 17.2667 13.72 16.9067 12.88L11.7333 0.92C11.4933 0.36 10.9467 0 10.3333 0C9.72 0 9.17333 0.36 8.93333 0.92L3.76 12.88C3.4 13.72 4.02667 14.6667 4.94667 14.6667C5.46667 14.6667 5.93333 14.3467 6.13333 13.8667L7 11.7333ZM10.3333 2.64L12.8267 9.33333H7.84L10.3333 2.64Z" fill="#C1C6CD"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1456_146350">
|
||||||
|
<rect width="20" height="20" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 818 B |
11
res/img/element-icons/room/composer/rich_text.svg
Normal file
11
res/img/element-icons/room/composer/rich_text.svg
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_1456_146365)">
|
||||||
|
<path d="M7.00042 13.7333H13.6671L14.5471 15.8667C14.7471 16.3467 15.2137 16.6667 15.7337 16.6667C16.6537 16.6667 17.2671 15.72 16.9071 14.88L11.7337 2.92C11.4937 2.36 10.9471 2 10.3337 2C9.72042 2 9.17375 2.36 8.93375 2.92L3.76042 14.88C3.40042 15.72 4.02708 16.6667 4.94708 16.6667C5.46708 16.6667 5.93375 16.3467 6.13375 15.8667L7.00042 13.7333ZM10.3337 4.64L12.8271 11.3333H7.84042L10.3337 4.64Z" fill="#C1C6CD"/>
|
||||||
|
<path d="M0.5 9.66927C0.5 10.6787 1.32386 11.5026 2.33333 11.5026H18.3333C19.3428 11.5026 20.1667 10.6787 20.1667 9.66927C20.1667 8.6598 19.3428 7.83594 18.3333 7.83594H2.33333C1.32386 7.83594 0.5 8.6598 0.5 9.66927Z" fill="#C1C6CD" stroke="white"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1456_146365">
|
||||||
|
<rect width="20" height="20" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 921 B |
|
@ -60,6 +60,7 @@ import {
|
||||||
} from '../../../voice-broadcast';
|
} from '../../../voice-broadcast';
|
||||||
import { SendWysiwygComposer, sendMessage } from './wysiwyg_composer/';
|
import { SendWysiwygComposer, sendMessage } from './wysiwyg_composer/';
|
||||||
import { MatrixClientProps, withMatrixClientHOC } from '../../../contexts/MatrixClientContext';
|
import { MatrixClientProps, withMatrixClientHOC } from '../../../contexts/MatrixClientContext';
|
||||||
|
import { htmlToPlainText } from '../../../utils/room/htmlToPlaintext';
|
||||||
|
|
||||||
let instanceCount = 0;
|
let instanceCount = 0;
|
||||||
|
|
||||||
|
@ -100,6 +101,9 @@ interface IState {
|
||||||
showStickersButton: boolean;
|
showStickersButton: boolean;
|
||||||
showPollsButton: boolean;
|
showPollsButton: boolean;
|
||||||
showVoiceBroadcastButton: boolean;
|
showVoiceBroadcastButton: boolean;
|
||||||
|
isWysiwygLabEnabled: boolean;
|
||||||
|
isRichTextEnabled: boolean;
|
||||||
|
initialComposerContent: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MessageComposer extends React.Component<IProps, IState> {
|
export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
|
@ -117,6 +121,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
public static defaultProps = {
|
public static defaultProps = {
|
||||||
compact: false,
|
compact: false,
|
||||||
showVoiceBroadcastButton: false,
|
showVoiceBroadcastButton: false,
|
||||||
|
isRichTextEnabled: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
public constructor(props: IProps) {
|
public constructor(props: IProps) {
|
||||||
|
@ -133,6 +138,9 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
showStickersButton: SettingsStore.getValue("MessageComposerInput.showStickersButton"),
|
showStickersButton: SettingsStore.getValue("MessageComposerInput.showStickersButton"),
|
||||||
showPollsButton: SettingsStore.getValue("MessageComposerInput.showPollsButton"),
|
showPollsButton: SettingsStore.getValue("MessageComposerInput.showPollsButton"),
|
||||||
showVoiceBroadcastButton: SettingsStore.getValue(Features.VoiceBroadcast),
|
showVoiceBroadcastButton: SettingsStore.getValue(Features.VoiceBroadcast),
|
||||||
|
isWysiwygLabEnabled: SettingsStore.getValue<boolean>("feature_wysiwyg_composer"),
|
||||||
|
isRichTextEnabled: true,
|
||||||
|
initialComposerContent: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
this.instanceId = instanceCount++;
|
this.instanceId = instanceCount++;
|
||||||
|
@ -140,6 +148,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
SettingsStore.monitorSetting("MessageComposerInput.showStickersButton", null);
|
SettingsStore.monitorSetting("MessageComposerInput.showStickersButton", null);
|
||||||
SettingsStore.monitorSetting("MessageComposerInput.showPollsButton", null);
|
SettingsStore.monitorSetting("MessageComposerInput.showPollsButton", null);
|
||||||
SettingsStore.monitorSetting(Features.VoiceBroadcast, null);
|
SettingsStore.monitorSetting(Features.VoiceBroadcast, null);
|
||||||
|
SettingsStore.monitorSetting("feature_wysiwyg_composer", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get voiceRecording(): Optional<VoiceMessageRecording> {
|
private get voiceRecording(): Optional<VoiceMessageRecording> {
|
||||||
|
@ -220,6 +229,12 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "feature_wysiwyg_composer": {
|
||||||
|
if (this.state.isWysiwygLabEnabled !== settingUpdatedPayload.newValue) {
|
||||||
|
this.setState({ isWysiwygLabEnabled: Boolean(settingUpdatedPayload.newValue) });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -318,12 +333,13 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
this.messageComposerInput.current?.sendMessage();
|
this.messageComposerInput.current?.sendMessage();
|
||||||
|
|
||||||
const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer");
|
if (this.state.isWysiwygLabEnabled) {
|
||||||
if (isWysiwygComposerEnabled) {
|
|
||||||
const { permalinkCreator, relation, replyToEvent } = this.props;
|
const { permalinkCreator, relation, replyToEvent } = this.props;
|
||||||
sendMessage(this.state.composerContent,
|
sendMessage(this.state.composerContent,
|
||||||
|
this.state.isRichTextEnabled,
|
||||||
{ mxClient: this.props.mxClient, roomContext: this.context, permalinkCreator, relation, replyToEvent });
|
{ mxClient: this.props.mxClient, roomContext: this.context, permalinkCreator, relation, replyToEvent });
|
||||||
dis.dispatch({ action: Action.ClearAndFocusSendMessageComposer });
|
dis.dispatch({ action: Action.ClearAndFocusSendMessageComposer });
|
||||||
|
this.setState({ composerContent: '', initialComposerContent: '' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -340,6 +356,16 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onRichTextToggle = () => {
|
||||||
|
this.setState(state => ({
|
||||||
|
isRichTextEnabled: !state.isRichTextEnabled,
|
||||||
|
initialComposerContent: !state.isRichTextEnabled ?
|
||||||
|
state.composerContent :
|
||||||
|
// TODO when available use rust model plain text
|
||||||
|
htmlToPlainText(state.composerContent),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
private onVoiceStoreUpdate = () => {
|
private onVoiceStoreUpdate = () => {
|
||||||
this.updateRecordingState();
|
this.updateRecordingState();
|
||||||
};
|
};
|
||||||
|
@ -395,7 +421,6 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer");
|
|
||||||
const controls = [
|
const controls = [
|
||||||
this.props.e2eStatus ?
|
this.props.e2eStatus ?
|
||||||
<E2EIcon key="e2eIcon" status={this.props.e2eStatus} className="mx_MessageComposer_e2eIcon" /> :
|
<E2EIcon key="e2eIcon" status={this.props.e2eStatus} className="mx_MessageComposer_e2eIcon" /> :
|
||||||
|
@ -410,12 +435,14 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const canSendMessages = this.context.canSendMessages && !this.context.tombstone;
|
const canSendMessages = this.context.canSendMessages && !this.context.tombstone;
|
||||||
if (canSendMessages) {
|
if (canSendMessages) {
|
||||||
if (isWysiwygComposerEnabled) {
|
if (this.state.isWysiwygLabEnabled) {
|
||||||
controls.push(
|
controls.push(
|
||||||
<SendWysiwygComposer key="controls_input"
|
<SendWysiwygComposer key="controls_input"
|
||||||
disabled={this.state.haveRecording}
|
disabled={this.state.haveRecording}
|
||||||
onChange={this.onWysiwygChange}
|
onChange={this.onWysiwygChange}
|
||||||
onSend={this.sendMessage}
|
onSend={this.sendMessage}
|
||||||
|
isRichTextEnabled={this.state.isRichTextEnabled}
|
||||||
|
initialContent={this.state.initialComposerContent}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -503,7 +530,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
"mx_MessageComposer": true,
|
"mx_MessageComposer": true,
|
||||||
"mx_MessageComposer--compact": this.props.compact,
|
"mx_MessageComposer--compact": this.props.compact,
|
||||||
"mx_MessageComposer_e2eStatus": this.props.e2eStatus != undefined,
|
"mx_MessageComposer_e2eStatus": this.props.e2eStatus != undefined,
|
||||||
"mx_MessageComposer_wysiwyg": isWysiwygComposerEnabled,
|
"mx_MessageComposer_wysiwyg": this.state.isWysiwygLabEnabled && this.state.isRichTextEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -532,6 +559,9 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
showLocationButton={!window.electron}
|
showLocationButton={!window.electron}
|
||||||
showPollsButton={this.state.showPollsButton}
|
showPollsButton={this.state.showPollsButton}
|
||||||
showStickersButton={this.showStickersButton}
|
showStickersButton={this.showStickersButton}
|
||||||
|
showComposerModeButton={this.state.isWysiwygLabEnabled}
|
||||||
|
isRichTextEnabled={this.state.isRichTextEnabled}
|
||||||
|
onComposerModeClick={this.onRichTextToggle}
|
||||||
toggleButtonMenu={this.toggleButtonMenu}
|
toggleButtonMenu={this.toggleButtonMenu}
|
||||||
showVoiceBroadcastButton={this.state.showVoiceBroadcastButton}
|
showVoiceBroadcastButton={this.state.showVoiceBroadcastButton}
|
||||||
onStartVoiceBroadcastClick={() => {
|
onStartVoiceBroadcastClick={() => {
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { IEventRelation } from "matrix-js-sdk/src/models/event";
|
import { IEventRelation } from "matrix-js-sdk/src/models/event";
|
||||||
import { M_POLL_START } from "matrix-events-sdk";
|
import { M_POLL_START } from "matrix-events-sdk";
|
||||||
import React, { createContext, ReactElement, useContext, useRef } from 'react';
|
import React, { createContext, MouseEventHandler, ReactElement, useContext, useRef } from 'react';
|
||||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
import { THREAD_RELATION_TYPE } from 'matrix-js-sdk/src/models/thread';
|
import { THREAD_RELATION_TYPE } from 'matrix-js-sdk/src/models/thread';
|
||||||
|
@ -55,6 +55,9 @@ interface IProps {
|
||||||
toggleButtonMenu: () => void;
|
toggleButtonMenu: () => void;
|
||||||
showVoiceBroadcastButton: boolean;
|
showVoiceBroadcastButton: boolean;
|
||||||
onStartVoiceBroadcastClick: () => void;
|
onStartVoiceBroadcastClick: () => void;
|
||||||
|
isRichTextEnabled: boolean;
|
||||||
|
showComposerModeButton: boolean;
|
||||||
|
onComposerModeClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type OverflowMenuCloser = () => void;
|
type OverflowMenuCloser = () => void;
|
||||||
|
@ -85,6 +88,8 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
||||||
} else {
|
} else {
|
||||||
mainButtons = [
|
mainButtons = [
|
||||||
emojiButton(props),
|
emojiButton(props),
|
||||||
|
props.showComposerModeButton &&
|
||||||
|
<ComposerModeButton key="composerModeButton" isRichTextEnabled={props.isRichTextEnabled} onClick={props.onComposerModeClick} />,
|
||||||
uploadButton(), // props passed via UploadButtonContext
|
uploadButton(), // props passed via UploadButtonContext
|
||||||
];
|
];
|
||||||
moreButtons = [
|
moreButtons = [
|
||||||
|
@ -397,4 +402,23 @@ function showLocationButton(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface WysiwygToggleButtonProps {
|
||||||
|
isRichTextEnabled: boolean;
|
||||||
|
onClick: MouseEventHandler<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ComposerModeButton({ isRichTextEnabled, onClick }: WysiwygToggleButtonProps) {
|
||||||
|
const title = isRichTextEnabled ? _t("Show plain text") : _t("Show formatting");
|
||||||
|
|
||||||
|
return <CollapsibleButton
|
||||||
|
className="mx_MessageComposer_button"
|
||||||
|
iconClassName={classNames({
|
||||||
|
"mx_MessageComposer_plain_text": isRichTextEnabled,
|
||||||
|
"mx_MessageComposer_rich_text": !isRichTextEnabled,
|
||||||
|
})}
|
||||||
|
onClick={onClick}
|
||||||
|
title={title}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
export default MessageComposerButtons;
|
export default MessageComposerButtons;
|
||||||
|
|
|
@ -15,32 +15,38 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { forwardRef, RefObject } from 'react';
|
import React, { forwardRef, RefObject } from 'react';
|
||||||
import { FormattingFunctions } from '@matrix-org/matrix-wysiwyg';
|
|
||||||
|
|
||||||
import { useWysiwygSendActionHandler } from './hooks/useWysiwygSendActionHandler';
|
import { useWysiwygSendActionHandler } from './hooks/useWysiwygSendActionHandler';
|
||||||
import { WysiwygComposer } from './components/WysiwygComposer';
|
import { WysiwygComposer } from './components/WysiwygComposer';
|
||||||
|
import { PlainTextComposer } from './components/PlainTextComposer';
|
||||||
|
import { ComposerFunctions } from './types';
|
||||||
|
|
||||||
interface SendWysiwygComposerProps {
|
|
||||||
disabled?: boolean;
|
|
||||||
onChange: (content: string) => void;
|
|
||||||
onSend: () => void;
|
|
||||||
}
|
|
||||||
interface ContentProps {
|
interface ContentProps {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
formattingFunctions: FormattingFunctions;
|
composerFunctions: ComposerFunctions;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Content = forwardRef<HTMLElement, ContentProps>(
|
const Content = forwardRef<HTMLElement, ContentProps>(
|
||||||
function Content({ disabled, formattingFunctions: wysiwyg }: ContentProps, forwardRef: RefObject<HTMLElement>) {
|
function Content({ disabled, composerFunctions }: ContentProps, forwardRef: RefObject<HTMLElement>) {
|
||||||
useWysiwygSendActionHandler(disabled, forwardRef, wysiwyg);
|
useWysiwygSendActionHandler(disabled, forwardRef, composerFunctions);
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export function SendWysiwygComposer(props: SendWysiwygComposerProps) {
|
interface SendWysiwygComposerProps {
|
||||||
return (
|
initialContent?: string;
|
||||||
<WysiwygComposer className="mx_SendWysiwygComposer" {...props}>{ (ref, wysiwyg) => (
|
isRichTextEnabled: boolean;
|
||||||
<Content disabled={props.disabled} ref={ref} formattingFunctions={wysiwyg} />
|
disabled?: boolean;
|
||||||
) }
|
onChange: (content: string) => void;
|
||||||
</WysiwygComposer>);
|
onSend: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SendWysiwygComposer({ isRichTextEnabled, ...props }: SendWysiwygComposerProps) {
|
||||||
|
const Composer = isRichTextEnabled ? WysiwygComposer : PlainTextComposer;
|
||||||
|
|
||||||
|
return <Composer className="mx_SendWysiwygComposer" {...props}>
|
||||||
|
{ (ref, composerFunctions) => (
|
||||||
|
<Content disabled={props.disabled} ref={ref} composerFunctions={composerFunctions} />
|
||||||
|
) }
|
||||||
|
</Composer>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
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, { MutableRefObject, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { useComposerFunctions } from '../hooks/useComposerFunctions';
|
||||||
|
import { usePlainTextInitialization } from '../hooks/usePlainTextInitialization';
|
||||||
|
import { usePlainTextListeners } from '../hooks/usePlainTextListeners';
|
||||||
|
import { useSetCursorPosition } from '../hooks/useSetCursorPosition';
|
||||||
|
import { ComposerFunctions } from '../types';
|
||||||
|
import { Editor } from "./Editor";
|
||||||
|
|
||||||
|
interface PlainTextComposerProps {
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange?: (content: string) => void;
|
||||||
|
onSend: () => void;
|
||||||
|
initialContent?: string;
|
||||||
|
className?: string;
|
||||||
|
children?: (
|
||||||
|
ref: MutableRefObject<HTMLDivElement | null>,
|
||||||
|
composerFunctions: ComposerFunctions,
|
||||||
|
) => ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PlainTextComposer({
|
||||||
|
className, disabled, onSend, onChange, children, initialContent }: PlainTextComposerProps,
|
||||||
|
) {
|
||||||
|
const { ref, onInput, onPaste, onKeyDown } = usePlainTextListeners(onChange, onSend);
|
||||||
|
const composerFunctions = useComposerFunctions(ref);
|
||||||
|
usePlainTextInitialization(initialContent, ref);
|
||||||
|
useSetCursorPosition(disabled, ref);
|
||||||
|
|
||||||
|
return <div
|
||||||
|
data-testid="PlainTextComposer"
|
||||||
|
className={className}
|
||||||
|
onInput={onInput}
|
||||||
|
onPaste={onPaste}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
>
|
||||||
|
<Editor ref={ref} disabled={disabled} />
|
||||||
|
{ children?.(ref, composerFunctions) }
|
||||||
|
</div>;
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ import { useWysiwyg, FormattingFunctions } from "@matrix-org/matrix-wysiwyg";
|
||||||
import { FormattingButtons } from './FormattingButtons';
|
import { FormattingButtons } from './FormattingButtons';
|
||||||
import { Editor } from './Editor';
|
import { Editor } from './Editor';
|
||||||
import { useInputEventProcessor } from '../hooks/useInputEventProcessor';
|
import { useInputEventProcessor } from '../hooks/useInputEventProcessor';
|
||||||
|
import { useSetCursorPosition } from '../hooks/useSetCursorPosition';
|
||||||
|
|
||||||
interface WysiwygComposerProps {
|
interface WysiwygComposerProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
@ -47,10 +48,13 @@ export const WysiwygComposer = memo(function WysiwygComposer(
|
||||||
}
|
}
|
||||||
}, [onChange, content, disabled]);
|
}, [onChange, content, disabled]);
|
||||||
|
|
||||||
|
const isReady = isWysiwygReady && !disabled;
|
||||||
|
useSetCursorPosition(!isReady, ref);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div data-testid="WysiwygComposer" className={className}>
|
||||||
<FormattingButtons composer={wysiwyg} formattingStates={formattingStates} />
|
<FormattingButtons composer={wysiwyg} formattingStates={formattingStates} />
|
||||||
<Editor ref={ref} disabled={!isWysiwygReady || disabled} />
|
<Editor ref={ref} disabled={!isReady} />
|
||||||
{ children?.(ref, wysiwyg) }
|
{ children?.(ref, wysiwyg) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
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 { RefObject, useMemo } from "react";
|
||||||
|
|
||||||
|
export function useComposerFunctions(ref: RefObject<HTMLDivElement>) {
|
||||||
|
return useMemo(() => ({
|
||||||
|
clear: () => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.innerHTML = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}), [ref]);
|
||||||
|
}
|
|
@ -20,7 +20,7 @@ import { useCallback } from "react";
|
||||||
import { useSettingValue } from "../../../../../hooks/useSettings";
|
import { useSettingValue } from "../../../../../hooks/useSettings";
|
||||||
|
|
||||||
export function useInputEventProcessor(onSend: () => void) {
|
export function useInputEventProcessor(onSend: () => void) {
|
||||||
const isCtrlEnter = useSettingValue("MessageComposerInput.ctrlEnterToSend") as boolean;
|
const isCtrlEnter = useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
|
||||||
return useCallback((event: WysiwygInputEvent) => {
|
return useCallback((event: WysiwygInputEvent) => {
|
||||||
if (event instanceof ClipboardEvent) {
|
if (event instanceof ClipboardEvent) {
|
||||||
return event;
|
return event;
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
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 { RefObject, useEffect } from "react";
|
||||||
|
|
||||||
|
export function usePlainTextInitialization(initialContent: string, ref: RefObject<HTMLElement>) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.innerText = initialContent;
|
||||||
|
}
|
||||||
|
}, [ref, initialContent]);
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
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 { KeyboardEvent, SyntheticEvent, useCallback, useRef } from "react";
|
||||||
|
|
||||||
|
import { useSettingValue } from "../../../../../hooks/useSettings";
|
||||||
|
|
||||||
|
function isDivElement(target: EventTarget): target is HTMLDivElement {
|
||||||
|
return target instanceof HTMLDivElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePlainTextListeners(onChange: (content: string) => void, onSend: () => void) {
|
||||||
|
const ref = useRef<HTMLDivElement>();
|
||||||
|
const send = useCallback((() => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.innerHTML = '';
|
||||||
|
}
|
||||||
|
onSend();
|
||||||
|
}), [ref, onSend]);
|
||||||
|
|
||||||
|
const onInput = useCallback((event: SyntheticEvent<HTMLDivElement, InputEvent | ClipboardEvent>) => {
|
||||||
|
if (isDivElement(event.target)) {
|
||||||
|
onChange(event.target.innerHTML);
|
||||||
|
}
|
||||||
|
}, [onChange]);
|
||||||
|
|
||||||
|
const isCtrlEnter = useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
|
||||||
|
const onKeyDown = useCallback((event: KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (event.key === 'Enter' && !event.shiftKey && (!isCtrlEnter || (isCtrlEnter && event.ctrlKey))) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
send();
|
||||||
|
}
|
||||||
|
}, [isCtrlEnter, send]);
|
||||||
|
|
||||||
|
return { ref, onInput, onPaste: onInput, onKeyDown };
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
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 { RefObject, useEffect } from "react";
|
||||||
|
|
||||||
|
import { setCursorPositionAtTheEnd } from "./utils";
|
||||||
|
|
||||||
|
export function useSetCursorPosition(disabled: boolean, ref: RefObject<HTMLElement>) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current && !disabled) {
|
||||||
|
setCursorPositionAtTheEnd(ref.current);
|
||||||
|
}
|
||||||
|
}, [ref, disabled]);
|
||||||
|
}
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { RefObject, useCallback, useRef } from "react";
|
import { RefObject, useCallback, useRef } from "react";
|
||||||
import { FormattingFunctions } from "@matrix-org/matrix-wysiwyg";
|
|
||||||
|
|
||||||
import defaultDispatcher from "../../../../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../../../../dispatcher/dispatcher";
|
||||||
import { Action } from "../../../../../dispatcher/actions";
|
import { Action } from "../../../../../dispatcher/actions";
|
||||||
|
@ -23,11 +22,12 @@ import { ActionPayload } from "../../../../../dispatcher/payloads";
|
||||||
import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/RoomContext";
|
import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/RoomContext";
|
||||||
import { useDispatcher } from "../../../../../hooks/useDispatcher";
|
import { useDispatcher } from "../../../../../hooks/useDispatcher";
|
||||||
import { focusComposer } from "./utils";
|
import { focusComposer } from "./utils";
|
||||||
|
import { ComposerFunctions } from "../types";
|
||||||
|
|
||||||
export function useWysiwygSendActionHandler(
|
export function useWysiwygSendActionHandler(
|
||||||
disabled: boolean,
|
disabled: boolean,
|
||||||
composerElement: RefObject<HTMLElement>,
|
composerElement: RefObject<HTMLElement>,
|
||||||
wysiwyg: FormattingFunctions,
|
composerFunctions: ComposerFunctions,
|
||||||
) {
|
) {
|
||||||
const roomContext = useRoomContext();
|
const roomContext = useRoomContext();
|
||||||
const timeoutId = useRef<number>();
|
const timeoutId = useRef<number>();
|
||||||
|
@ -45,12 +45,12 @@ export function useWysiwygSendActionHandler(
|
||||||
focusComposer(composerElement, context, roomContext, timeoutId);
|
focusComposer(composerElement, context, roomContext, timeoutId);
|
||||||
break;
|
break;
|
||||||
case Action.ClearAndFocusSendMessageComposer:
|
case Action.ClearAndFocusSendMessageComposer:
|
||||||
wysiwyg.clear();
|
composerFunctions.clear();
|
||||||
focusComposer(composerElement, context, roomContext, timeoutId);
|
focusComposer(composerElement, context, roomContext, timeoutId);
|
||||||
break;
|
break;
|
||||||
// TODO: case Action.ComposerInsert: - see SendMessageComposer
|
// TODO: case Action.ComposerInsert: - see SendMessageComposer
|
||||||
}
|
}
|
||||||
}, [disabled, composerElement, wysiwyg, timeoutId, roomContext]);
|
}, [disabled, composerElement, composerFunctions, timeoutId, roomContext]);
|
||||||
|
|
||||||
useDispatcher(defaultDispatcher, handler);
|
useDispatcher(defaultDispatcher, handler);
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,3 +41,14 @@ export function focusComposer(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setCursorPositionAtTheEnd(element: HTMLElement) {
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(element);
|
||||||
|
range.collapse(false);
|
||||||
|
const selection = document.getSelection();
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
|
||||||
|
element.focus();
|
||||||
|
}
|
||||||
|
|
19
src/components/views/rooms/wysiwyg_composer/types.ts
Normal file
19
src/components/views/rooms/wysiwyg_composer/types.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
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 type ComposerFunctions = {
|
||||||
|
clear: () => void;
|
||||||
|
};
|
|
@ -16,8 +16,11 @@ limitations under the License.
|
||||||
|
|
||||||
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 { 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 {
|
||||||
|
@ -39,6 +42,18 @@ function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
||||||
return (mxReply && mxReply.outerHTML) || "";
|
return (mxReply && mxReply.outerHTML) || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTextReplyFallback(mxEvent: MatrixEvent): string {
|
||||||
|
const body = mxEvent.getContent().body;
|
||||||
|
if (typeof body !== 'string') {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const lines = body.split("\n").map(l => l.trim());
|
||||||
|
if (lines.length > 2 && lines[0].startsWith("> ") && lines[1].length === 0) {
|
||||||
|
return `${lines[0]}\n\n`;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
interface CreateMessageContentParams {
|
interface CreateMessageContentParams {
|
||||||
relation?: IEventRelation;
|
relation?: IEventRelation;
|
||||||
replyToEvent?: MatrixEvent;
|
replyToEvent?: MatrixEvent;
|
||||||
|
@ -49,6 +64,7 @@ interface CreateMessageContentParams {
|
||||||
|
|
||||||
export function createMessageContent(
|
export function createMessageContent(
|
||||||
message: string,
|
message: string,
|
||||||
|
isHTML: boolean,
|
||||||
{ relation, replyToEvent, permalinkCreator, includeReplyLegacyFallback = true, editedEvent }:
|
{ relation, replyToEvent, permalinkCreator, includeReplyLegacyFallback = true, editedEvent }:
|
||||||
CreateMessageContentParams,
|
CreateMessageContentParams,
|
||||||
): IContent {
|
): IContent {
|
||||||
|
@ -56,6 +72,7 @@ export function createMessageContent(
|
||||||
|
|
||||||
const isEditing = Boolean(editedEvent);
|
const isEditing = Boolean(editedEvent);
|
||||||
const isReply = isEditing ? Boolean(editedEvent?.replyEventId) : Boolean(replyToEvent);
|
const isReply = isEditing ? Boolean(editedEvent?.replyEventId) : Boolean(replyToEvent);
|
||||||
|
const isReplyAndEditing = isEditing && isReply;
|
||||||
|
|
||||||
/*const isEmote = containsEmote(model);
|
/*const isEmote = containsEmote(model);
|
||||||
if (isEmote) {
|
if (isEmote) {
|
||||||
|
@ -67,37 +84,44 @@ export function createMessageContent(
|
||||||
model = unescapeMessage(model);*/
|
model = unescapeMessage(model);*/
|
||||||
|
|
||||||
// const body = textSerialize(model);
|
// const body = textSerialize(model);
|
||||||
const body = message;
|
|
||||||
|
// TODO remove this ugly hack for replace br tag
|
||||||
|
const body = isHTML && htmlToPlainText(message) || message.replace(/<br>/g, '\n');
|
||||||
|
const bodyPrefix = isReplyAndEditing && getTextReplyFallback(editedEvent) || '';
|
||||||
|
const formattedBodyPrefix = isReplyAndEditing && getHtmlReplyFallback(editedEvent) || '';
|
||||||
|
|
||||||
const content: IContent = {
|
const content: IContent = {
|
||||||
// TODO emote
|
// TODO emote
|
||||||
// msgtype: isEmote ? "m.emote" : "m.text",
|
|
||||||
msgtype: MsgType.Text,
|
msgtype: MsgType.Text,
|
||||||
body: body,
|
// TODO when available, use HTML --> Plain text conversion from wysiwyg rust model
|
||||||
|
body: isEditing ? `${bodyPrefix} * ${body}` : body,
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO markdown support
|
// TODO markdown support
|
||||||
|
|
||||||
/*const formattedBody = htmlSerializeIfNeeded(model, {
|
const isMarkdownEnabled = SettingsStore.getValue<boolean>("MessageComposerInput.useMarkdown");
|
||||||
forceHTML: !!replyToEvent,
|
const formattedBody =
|
||||||
useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
|
isHTML ?
|
||||||
});*/
|
message :
|
||||||
const formattedBody = message;
|
isMarkdownEnabled ?
|
||||||
|
htmlSerializeFromMdIfNeeded(message, { forceHTML: isReply }) :
|
||||||
|
null;
|
||||||
|
|
||||||
if (formattedBody) {
|
if (formattedBody) {
|
||||||
content.format = "org.matrix.custom.html";
|
content.format = "org.matrix.custom.html";
|
||||||
|
content.formatted_body = isEditing ? `${formattedBodyPrefix} * ${formattedBody}` : formattedBody;
|
||||||
const htmlPrefix = isReply && isEditing ? getHtmlReplyFallback(editedEvent) : '';
|
|
||||||
content.formatted_body = isEditing ? `${htmlPrefix} * ${formattedBody}` : formattedBody;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
content['m.new_content'] = {
|
content['m.new_content'] = {
|
||||||
"msgtype": content.msgtype,
|
"msgtype": content.msgtype,
|
||||||
"body": body,
|
"body": body,
|
||||||
"format": "org.matrix.custom.html",
|
|
||||||
'formatted_body': formattedBody,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (formattedBody) {
|
||||||
|
content['m.new_content'].format = "org.matrix.custom.html";
|
||||||
|
content['m.new_content']['formatted_body'] = formattedBody;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newRelation = isEditing ?
|
const newRelation = isEditing ?
|
||||||
|
|
|
@ -44,7 +44,8 @@ interface SendMessageParams {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendMessage(
|
export function sendMessage(
|
||||||
html: string,
|
message: string,
|
||||||
|
isHTML: boolean,
|
||||||
{ roomContext, mxClient, ...params }: SendMessageParams,
|
{ roomContext, mxClient, ...params }: SendMessageParams,
|
||||||
) {
|
) {
|
||||||
const { relation, replyToEvent } = params;
|
const { relation, replyToEvent } = params;
|
||||||
|
@ -76,7 +77,8 @@ export function sendMessage(
|
||||||
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
content = createMessageContent(
|
content = createMessageContent(
|
||||||
html,
|
message,
|
||||||
|
isHTML,
|
||||||
params,
|
params,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -167,7 +169,7 @@ export function editMessage(
|
||||||
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, { editedEvent });
|
const editContent = createMessageContent(html, true, { editedEvent });
|
||||||
const newContent = editContent["m.new_content"];
|
const newContent = editContent["m.new_content"];
|
||||||
|
|
||||||
const shouldSend = true;
|
const shouldSend = true;
|
||||||
|
|
|
@ -1881,6 +1881,8 @@
|
||||||
"Voice Message": "Voice Message",
|
"Voice Message": "Voice Message",
|
||||||
"You do not have permission to start polls in this room.": "You do not have permission to start polls in this room.",
|
"You do not have permission to start polls in this room.": "You do not have permission to start polls in this room.",
|
||||||
"Poll": "Poll",
|
"Poll": "Poll",
|
||||||
|
"Show plain text": "Show plain text",
|
||||||
|
"Show formatting": "Show formatting",
|
||||||
"Bold": "Bold",
|
"Bold": "Bold",
|
||||||
"Italics": "Italics",
|
"Italics": "Italics",
|
||||||
"Strikethrough": "Strikethrough",
|
"Strikethrough": "Strikethrough",
|
||||||
|
|
19
src/utils/room/htmlToPlaintext.ts
Normal file
19
src/utils/room/htmlToPlaintext.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
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;
|
||||||
|
}
|
|
@ -163,7 +163,7 @@ describe('EditWysiwygComposer', () => {
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
const expectedContent = {
|
const expectedContent = {
|
||||||
"body": mockContent,
|
"body": ` * ${mockContent}`,
|
||||||
"format": "org.matrix.custom.html",
|
"format": "org.matrix.custom.html",
|
||||||
"formatted_body": ` * ${mockContent}`,
|
"formatted_body": ` * ${mockContent}`,
|
||||||
"m.new_content": {
|
"m.new_content": {
|
||||||
|
@ -186,6 +186,7 @@ describe('EditWysiwygComposer', () => {
|
||||||
it('Should focus when receiving an Action.FocusEditMessageComposer action', async () => {
|
it('Should focus when receiving an Action.FocusEditMessageComposer action', async () => {
|
||||||
// Given we don't have focus
|
// Given we don't have focus
|
||||||
customRender();
|
customRender();
|
||||||
|
screen.getByLabelText('Bold').focus();
|
||||||
expect(screen.getByRole('textbox')).not.toHaveFocus();
|
expect(screen.getByRole('textbox')).not.toHaveFocus();
|
||||||
|
|
||||||
// When we send the right action
|
// When we send the right action
|
||||||
|
@ -201,6 +202,7 @@ describe('EditWysiwygComposer', () => {
|
||||||
it('Should not focus when disabled', async () => {
|
it('Should not focus when disabled', async () => {
|
||||||
// Given we don't have focus and we are disabled
|
// Given we don't have focus and we are disabled
|
||||||
customRender(true);
|
customRender(true);
|
||||||
|
screen.getByLabelText('Bold').focus();
|
||||||
expect(screen.getByRole('textbox')).not.toHaveFocus();
|
expect(screen.getByRole('textbox')).not.toHaveFocus();
|
||||||
|
|
||||||
// When we send an action that would cause us to get focus
|
// When we send an action that would cause us to get focus
|
||||||
|
|
|
@ -24,8 +24,10 @@ import RoomContext from "../../../../../src/contexts/RoomContext";
|
||||||
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
|
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||||
import { Action } from "../../../../../src/dispatcher/actions";
|
import { Action } from "../../../../../src/dispatcher/actions";
|
||||||
import { IRoomState } from "../../../../../src/components/structures/RoomView";
|
import { IRoomState } from "../../../../../src/components/structures/RoomView";
|
||||||
import { createTestClient, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils";
|
import { createTestClient, flushPromises, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils";
|
||||||
import { SendWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer";
|
import { SendWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer";
|
||||||
|
import * as useComposerFunctions
|
||||||
|
from "../../../../../src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions";
|
||||||
|
|
||||||
const mockClear = jest.fn();
|
const mockClear = jest.fn();
|
||||||
|
|
||||||
|
@ -68,83 +70,112 @@ describe('SendWysiwygComposer', () => {
|
||||||
|
|
||||||
const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {});
|
const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {});
|
||||||
|
|
||||||
const customRender = (onChange = (_content: string) => void 0, onSend = () => void 0, disabled = false) => {
|
const customRender = (
|
||||||
|
onChange = (_content: string) => void 0,
|
||||||
|
onSend = () => void 0,
|
||||||
|
disabled = false,
|
||||||
|
isRichTextEnabled = true) => {
|
||||||
return render(
|
return render(
|
||||||
<MatrixClientContext.Provider value={mockClient}>
|
<MatrixClientContext.Provider value={mockClient}>
|
||||||
<RoomContext.Provider value={defaultRoomContext}>
|
<RoomContext.Provider value={defaultRoomContext}>
|
||||||
<SendWysiwygComposer onChange={onChange} onSend={onSend} disabled={disabled} />
|
<SendWysiwygComposer onChange={onChange} onSend={onSend} disabled={disabled} isRichTextEnabled={isRichTextEnabled} />
|
||||||
</RoomContext.Provider>
|
</RoomContext.Provider>
|
||||||
</MatrixClientContext.Provider>,
|
</MatrixClientContext.Provider>,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
it('Should focus when receiving an Action.FocusSendMessageComposer action', async () => {
|
it('Should render WysiwygComposer when isRichTextEnabled is at true', () => {
|
||||||
// Given we don't have focus
|
// When
|
||||||
customRender(jest.fn(), jest.fn());
|
customRender(jest.fn(), jest.fn(), false, true);
|
||||||
expect(screen.getByRole('textbox')).not.toHaveFocus();
|
|
||||||
|
|
||||||
// When we send the right action
|
// Then
|
||||||
defaultDispatcher.dispatch({
|
expect(screen.getByTestId('WysiwygComposer')).toBeTruthy();
|
||||||
action: Action.FocusSendMessageComposer,
|
|
||||||
context: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Then the component gets the focus
|
|
||||||
await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should focus and clear when receiving an Action.ClearAndFocusSendMessageComposer', async () => {
|
it('Should render PlainTextComposer when isRichTextEnabled is at false', () => {
|
||||||
// Given we don't have focus
|
// When
|
||||||
customRender(jest.fn(), jest.fn());
|
customRender(jest.fn(), jest.fn(), false, false);
|
||||||
expect(screen.getByRole('textbox')).not.toHaveFocus();
|
|
||||||
|
|
||||||
// When we send the right action
|
// Then
|
||||||
defaultDispatcher.dispatch({
|
expect(screen.getByTestId('PlainTextComposer')).toBeTruthy();
|
||||||
action: Action.ClearAndFocusSendMessageComposer,
|
|
||||||
context: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Then the component gets the focus
|
|
||||||
await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus());
|
|
||||||
expect(mockClear).toBeCalledTimes(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should focus when receiving a reply_to_event action', async () => {
|
describe.each([{ isRichTextEnabled: true }, { isRichTextEnabled: false }])(
|
||||||
// Given we don't have focus
|
'Should focus when receiving an Action.FocusSendMessageComposer action',
|
||||||
customRender(jest.fn(), jest.fn());
|
({ isRichTextEnabled }) => {
|
||||||
expect(screen.getByRole('textbox')).not.toHaveFocus();
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
// When we send the right action
|
it('Should focus when receiving an Action.FocusSendMessageComposer action', async () => {
|
||||||
defaultDispatcher.dispatch({
|
// Given we don't have focus
|
||||||
action: "reply_to_event",
|
customRender(jest.fn(), jest.fn(), false, isRichTextEnabled);
|
||||||
context: null,
|
|
||||||
|
// When we send the right action
|
||||||
|
defaultDispatcher.dispatch({
|
||||||
|
action: Action.FocusSendMessageComposer,
|
||||||
|
context: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then the component gets the focus
|
||||||
|
await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should focus and clear when receiving an Action.ClearAndFocusSendMessageComposer', async () => {
|
||||||
|
// Given we don't have focus
|
||||||
|
const mock = jest.spyOn(useComposerFunctions, 'useComposerFunctions');
|
||||||
|
mock.mockReturnValue({ clear: mockClear });
|
||||||
|
customRender(jest.fn(), jest.fn(), false, isRichTextEnabled);
|
||||||
|
|
||||||
|
// When we send the right action
|
||||||
|
defaultDispatcher.dispatch({
|
||||||
|
action: Action.ClearAndFocusSendMessageComposer,
|
||||||
|
context: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then the component gets the focus
|
||||||
|
await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus());
|
||||||
|
expect(mockClear).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
mock.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should focus when receiving a reply_to_event action', async () => {
|
||||||
|
// Given we don't have focus
|
||||||
|
customRender(jest.fn(), jest.fn(), false, isRichTextEnabled);
|
||||||
|
|
||||||
|
// When we send the right action
|
||||||
|
defaultDispatcher.dispatch({
|
||||||
|
action: "reply_to_event",
|
||||||
|
context: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then the component gets the focus
|
||||||
|
await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should not focus when disabled', async () => {
|
||||||
|
// Given we don't have focus and we are disabled
|
||||||
|
customRender(jest.fn(), jest.fn(), true, isRichTextEnabled);
|
||||||
|
expect(screen.getByRole('textbox')).not.toHaveFocus();
|
||||||
|
|
||||||
|
// When we send an action that would cause us to get focus
|
||||||
|
defaultDispatcher.dispatch({
|
||||||
|
action: Action.FocusSendMessageComposer,
|
||||||
|
context: null,
|
||||||
|
});
|
||||||
|
// (Send a second event to exercise the clearTimeout logic)
|
||||||
|
defaultDispatcher.dispatch({
|
||||||
|
action: Action.FocusSendMessageComposer,
|
||||||
|
context: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for event dispatch to happen
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
// Then we don't get it because we are disabled
|
||||||
|
expect(screen.getByRole('textbox')).not.toHaveFocus();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Then the component gets the focus
|
|
||||||
await waitFor(() => expect(screen.getByRole('textbox')).toHaveFocus());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Should not focus when disabled', async () => {
|
|
||||||
// Given we don't have focus and we are disabled
|
|
||||||
customRender(jest.fn(), jest.fn(), true);
|
|
||||||
expect(screen.getByRole('textbox')).not.toHaveFocus();
|
|
||||||
|
|
||||||
// When we send an action that would cause us to get focus
|
|
||||||
defaultDispatcher.dispatch({
|
|
||||||
action: Action.FocusSendMessageComposer,
|
|
||||||
context: null,
|
|
||||||
});
|
|
||||||
// (Send a second event to exercise the clearTimeout logic)
|
|
||||||
defaultDispatcher.dispatch({
|
|
||||||
action: Action.FocusSendMessageComposer,
|
|
||||||
context: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for event dispatch to happen
|
|
||||||
await new Promise((r) => setTimeout(r, 200));
|
|
||||||
|
|
||||||
// Then we don't get it because we are disabled
|
|
||||||
expect(screen.getByRole('textbox')).not.toHaveFocus();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
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 { render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
|
import { PlainTextComposer }
|
||||||
|
from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer";
|
||||||
|
|
||||||
|
// Work around missing ClipboardEvent type
|
||||||
|
class MyClipboardEvent {}
|
||||||
|
window.ClipboardEvent = MyClipboardEvent as any;
|
||||||
|
|
||||||
|
describe('PlainTextComposer', () => {
|
||||||
|
const customRender = (
|
||||||
|
onChange = (_content: string) => void 0,
|
||||||
|
onSend = () => void 0,
|
||||||
|
disabled = false,
|
||||||
|
initialContent?: string) => {
|
||||||
|
return render(
|
||||||
|
<PlainTextComposer onChange={onChange} onSend={onSend} disabled={disabled} initialContent={initialContent} />,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
it('Should have contentEditable at false when disabled', () => {
|
||||||
|
// When
|
||||||
|
customRender(jest.fn(), jest.fn(), true);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(screen.getByRole('textbox')).toHaveAttribute('contentEditable', "false");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should have focus', () => {
|
||||||
|
// When
|
||||||
|
customRender(jest.fn(), jest.fn(), false);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(screen.getByRole('textbox')).toHaveFocus();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should call onChange handler', async () => {
|
||||||
|
// When
|
||||||
|
const content = 'content';
|
||||||
|
const onChange = jest.fn();
|
||||||
|
customRender(onChange, jest.fn());
|
||||||
|
await userEvent.type(screen.getByRole('textbox'), content);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(onChange).toBeCalledWith(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should call onSend when Enter is pressed', async () => {
|
||||||
|
//When
|
||||||
|
const onSend = jest.fn();
|
||||||
|
customRender(jest.fn(), onSend);
|
||||||
|
await userEvent.type(screen.getByRole('textbox'), '{enter}');
|
||||||
|
|
||||||
|
// Then it sends a message
|
||||||
|
expect(onSend).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should clear textbox content when clear is called', async () => {
|
||||||
|
//When
|
||||||
|
let composer;
|
||||||
|
render(
|
||||||
|
<PlainTextComposer onChange={jest.fn()} onSend={jest.fn()}>
|
||||||
|
{ (ref, composerFunctions) => {
|
||||||
|
composer = composerFunctions;
|
||||||
|
return null;
|
||||||
|
} }
|
||||||
|
</PlainTextComposer>,
|
||||||
|
);
|
||||||
|
await userEvent.type(screen.getByRole('textbox'), 'content');
|
||||||
|
expect(screen.getByRole('textbox').innerHTML).toBe('content');
|
||||||
|
composer.clear();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(screen.getByRole('textbox').innerHTML).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -19,10 +19,6 @@ import React from "react";
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import { InputEventProcessor, Wysiwyg, WysiwygProps } from "@matrix-org/matrix-wysiwyg";
|
import { InputEventProcessor, Wysiwyg, WysiwygProps } from "@matrix-org/matrix-wysiwyg";
|
||||||
|
|
||||||
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
|
||||||
import { IRoomState } from "../../../../../../src/components/structures/RoomView";
|
|
||||||
import { createTestClient, getRoomContext, mkEvent, mkStubRoom } from "../../../../../test-utils";
|
|
||||||
import RoomContext from "../../../../../../src/contexts/RoomContext";
|
|
||||||
import { WysiwygComposer }
|
import { WysiwygComposer }
|
||||||
from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer";
|
from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer";
|
||||||
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
||||||
|
@ -54,32 +50,14 @@ jest.mock("@matrix-org/matrix-wysiwyg", () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('WysiwygComposer', () => {
|
describe('WysiwygComposer', () => {
|
||||||
afterEach(() => {
|
const customRender = (
|
||||||
jest.resetAllMocks();
|
onChange = (_content: string) => void 0,
|
||||||
});
|
onSend = () => void 0,
|
||||||
|
disabled = false,
|
||||||
const mockClient = createTestClient();
|
initialContent?: string) => {
|
||||||
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 = getRoomContext(mockRoom, {});
|
|
||||||
|
|
||||||
const customRender = (onChange = (_content: string) => void 0, onSend = () => void 0, disabled = false) => {
|
|
||||||
return render(
|
return render(
|
||||||
<MatrixClientContext.Provider value={mockClient}>
|
<WysiwygComposer onChange={onChange} onSend={onSend} disabled={disabled} initialContent={initialContent} />,
|
||||||
<RoomContext.Provider value={defaultRoomContext}>
|
|
||||||
<WysiwygComposer onChange={onChange} onSend={onSend} disabled={disabled} />
|
|
||||||
</RoomContext.Provider>
|
|
||||||
</MatrixClientContext.Provider>,
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -91,6 +69,14 @@ describe('WysiwygComposer', () => {
|
||||||
expect(screen.getByRole('textbox')).toHaveAttribute('contentEditable', "false");
|
expect(screen.getByRole('textbox')).toHaveAttribute('contentEditable', "false");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should have focus', () => {
|
||||||
|
// When
|
||||||
|
customRender(jest.fn(), jest.fn(), false);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(screen.getByRole('textbox')).toHaveFocus();
|
||||||
|
});
|
||||||
|
|
||||||
it('Should call onChange handler', (done) => {
|
it('Should call onChange handler', (done) => {
|
||||||
const html = '<b>html</b>';
|
const html = '<b>html</b>';
|
||||||
customRender((content) => {
|
customRender((content) => {
|
||||||
|
@ -104,7 +90,7 @@ describe('WysiwygComposer', () => {
|
||||||
const onSend = jest.fn();
|
const onSend = jest.fn();
|
||||||
customRender(jest.fn(), onSend);
|
customRender(jest.fn(), onSend);
|
||||||
|
|
||||||
// When we tell its inputEventProcesser that the user pressed Enter
|
// When we tell its inputEventProcessor that the user pressed Enter
|
||||||
const event = new InputEvent("insertParagraph", { inputType: "insertParagraph" });
|
const event = new InputEvent("insertParagraph", { inputType: "insertParagraph" });
|
||||||
const wysiwyg = { actions: { clear: () => {} } } as Wysiwyg;
|
const wysiwyg = { actions: { clear: () => {} } } as Wysiwyg;
|
||||||
inputEventProcessor(event, wysiwyg);
|
inputEventProcessor(event, wysiwyg);
|
||||||
|
|
|
@ -40,11 +40,11 @@ describe('createMessageContent', () => {
|
||||||
|
|
||||||
it("Should create html message", () => {
|
it("Should create html message", () => {
|
||||||
// When
|
// When
|
||||||
const content = createMessageContent(message, { permalinkCreator });
|
const content = createMessageContent(message, true, { permalinkCreator });
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(content).toEqual({
|
expect(content).toEqual({
|
||||||
"body": message,
|
"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",
|
||||||
|
@ -53,11 +53,11 @@ describe('createMessageContent', () => {
|
||||||
|
|
||||||
it('Should add reply to message content', () => {
|
it('Should add reply to message content', () => {
|
||||||
// When
|
// When
|
||||||
const content = createMessageContent(message, { permalinkCreator, replyToEvent: mockEvent });
|
const content = createMessageContent(message, true, { permalinkCreator, replyToEvent: mockEvent });
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(content).toEqual({
|
expect(content).toEqual({
|
||||||
"body": "> <myfakeuser> Replying to this\n\n<i><b>hello</b> world</i>",
|
"body": "> <myfakeuser> Replying to this\n\nhello world",
|
||||||
"format": "org.matrix.custom.html",
|
"format": "org.matrix.custom.html",
|
||||||
"formatted_body": "<mx-reply><blockquote><a href=\"$$permalink$$\">In reply to</a>" +
|
"formatted_body": "<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>"+
|
||||||
|
@ -77,11 +77,11 @@ describe('createMessageContent', () => {
|
||||||
rel_type: "m.thread",
|
rel_type: "m.thread",
|
||||||
event_id: "myFakeThreadId",
|
event_id: "myFakeThreadId",
|
||||||
};
|
};
|
||||||
const content = createMessageContent(message, { permalinkCreator, relation });
|
const content = createMessageContent(message, true, { permalinkCreator, relation });
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(content).toEqual({
|
expect(content).toEqual({
|
||||||
"body": message,
|
"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",
|
||||||
|
@ -110,16 +110,16 @@ describe('createMessageContent', () => {
|
||||||
event: true,
|
event: true,
|
||||||
});
|
});
|
||||||
const content =
|
const content =
|
||||||
createMessageContent(message, { permalinkCreator, editedEvent });
|
createMessageContent(message, true, { permalinkCreator, editedEvent });
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(content).toEqual({
|
expect(content).toEqual({
|
||||||
"body": message,
|
"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": message,
|
"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",
|
||||||
|
|
|
@ -65,7 +65,7 @@ describe('message', () => {
|
||||||
describe('sendMessage', () => {
|
describe('sendMessage', () => {
|
||||||
it('Should not send empty html message', async () => {
|
it('Should not send empty html message', async () => {
|
||||||
// When
|
// When
|
||||||
await sendMessage('', { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator });
|
await sendMessage('', true, { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator });
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(mockClient.sendMessage).toBeCalledTimes(0);
|
expect(mockClient.sendMessage).toBeCalledTimes(0);
|
||||||
|
@ -74,11 +74,15 @@ describe('message', () => {
|
||||||
|
|
||||||
it('Should send html message', async () => {
|
it('Should send html message', async () => {
|
||||||
// When
|
// When
|
||||||
await sendMessage(message, { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator });
|
await sendMessage(
|
||||||
|
message,
|
||||||
|
true,
|
||||||
|
{ roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator },
|
||||||
|
);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
const expectedContent = {
|
const expectedContent = {
|
||||||
"body": "<i><b>hello</b> world</i>",
|
"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",
|
||||||
|
@ -97,7 +101,7 @@ describe('message', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// When
|
// When
|
||||||
await sendMessage(message, {
|
await sendMessage(message, true, {
|
||||||
roomContext: defaultRoomContext,
|
roomContext: defaultRoomContext,
|
||||||
mxClient: mockClient,
|
mxClient: mockClient,
|
||||||
permalinkCreator,
|
permalinkCreator,
|
||||||
|
@ -112,7 +116,7 @@ describe('message', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const expectedContent = {
|
const expectedContent = {
|
||||||
"body": "> <myfakeuser2> My reply\n\n<i><b>hello</b> world</i>",
|
"body": "> <myfakeuser2> My reply\n\nhello world",
|
||||||
"format": "org.matrix.custom.html",
|
"format": "org.matrix.custom.html",
|
||||||
"formatted_body": "<mx-reply><blockquote><a href=\"$$permalink$$\">In reply to</a>" +
|
"formatted_body": "<mx-reply><blockquote><a href=\"$$permalink$$\">In reply to</a>" +
|
||||||
" <a href=\"https://matrix.to/#/myfakeuser2\">myfakeuser2</a>" +
|
" <a href=\"https://matrix.to/#/myfakeuser2\">myfakeuser2</a>" +
|
||||||
|
@ -130,7 +134,11 @@ describe('message', () => {
|
||||||
it('Should scroll to bottom after sending a html message', async () => {
|
it('Should scroll to bottom after sending a html message', async () => {
|
||||||
// When
|
// When
|
||||||
SettingsStore.setValue("scrollToBottomOnMessageSent", null, SettingLevel.DEVICE, true);
|
SettingsStore.setValue("scrollToBottomOnMessageSent", null, SettingLevel.DEVICE, true);
|
||||||
await sendMessage(message, { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator });
|
await sendMessage(
|
||||||
|
message,
|
||||||
|
true,
|
||||||
|
{ roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator },
|
||||||
|
);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(spyDispatcher).toBeCalledWith(
|
expect(spyDispatcher).toBeCalledWith(
|
||||||
|
@ -140,7 +148,11 @@ describe('message', () => {
|
||||||
|
|
||||||
it('Should handle emojis', async () => {
|
it('Should handle emojis', async () => {
|
||||||
// When
|
// When
|
||||||
await sendMessage('🎉', { roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator });
|
await sendMessage(
|
||||||
|
'🎉',
|
||||||
|
false,
|
||||||
|
{ roomContext: defaultRoomContext, mxClient: mockClient, permalinkCreator },
|
||||||
|
);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(spyDispatcher).toBeCalledWith(
|
expect(spyDispatcher).toBeCalledWith(
|
||||||
|
@ -203,7 +215,7 @@ describe('message', () => {
|
||||||
// Then
|
// Then
|
||||||
const { msgtype, format } = mockEvent.getContent();
|
const { msgtype, format } = mockEvent.getContent();
|
||||||
const expectedContent = {
|
const expectedContent = {
|
||||||
"body": newMessage,
|
"body": ` * ${newMessage}`,
|
||||||
"formatted_body": ` * ${newMessage}`,
|
"formatted_body": ` * ${newMessage}`,
|
||||||
"m.new_content": {
|
"m.new_content": {
|
||||||
"body": "Replying to this new content",
|
"body": "Replying to this new content",
|
||||||
|
|
Loading…
Reference in a new issue