mirror of https://github.com/vector-im/riot-web
First attempt to make the edition works in the WysiwygComposer
parent
a61076b4fb
commit
460f60e99d
|
@ -48,6 +48,7 @@ import RoomContext from "../../../contexts/RoomContext";
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import { options as linkifyOpts } from "../../../linkify-matrix";
|
import { options as linkifyOpts } from "../../../linkify-matrix";
|
||||||
import { getParentEventId } from '../../../utils/Reply';
|
import { getParentEventId } from '../../../utils/Reply';
|
||||||
|
import { EditWysiwygComposer } from '../rooms/wysiwyg_composer';
|
||||||
|
|
||||||
const MAX_HIGHLIGHT_LENGTH = 4096;
|
const MAX_HIGHLIGHT_LENGTH = 4096;
|
||||||
|
|
||||||
|
@ -562,7 +563,10 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.props.editState) {
|
if (this.props.editState) {
|
||||||
return <EditMessageComposer editState={this.props.editState} className="mx_EventTile_content" />;
|
const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer");
|
||||||
|
return isWysiwygComposerEnabled ?
|
||||||
|
<EditWysiwygComposer editorStateTransfer={this.props.editState} /> :
|
||||||
|
<EditMessageComposer editState={this.props.editState} className="mx_EventTile_content" />;
|
||||||
}
|
}
|
||||||
const mxEvent = this.props.mxEvent;
|
const mxEvent = this.props.mxEvent;
|
||||||
const content = mxEvent.getContent();
|
const content = mxEvent.getContent();
|
||||||
|
|
|
@ -833,6 +833,8 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
}
|
}
|
||||||
|
|
||||||
public insertPlaintext(text: string): void {
|
public insertPlaintext(text: string): void {
|
||||||
|
console.log('insertPlaintext', text);
|
||||||
|
debugger;
|
||||||
this.modifiedFlag = true;
|
this.modifiedFlag = true;
|
||||||
const { model } = this.props;
|
const { model } = this.props;
|
||||||
const { partCreator } = model;
|
const { partCreator } = model;
|
||||||
|
|
|
@ -350,6 +350,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
|
||||||
const event = this.props.editState.getEvent();
|
const event = this.props.editState.getEvent();
|
||||||
const threadId = event.threadRootId || null;
|
const threadId = event.threadRootId || null;
|
||||||
|
|
||||||
|
console.log('editContent', editContent);
|
||||||
this.props.mxClient.sendMessage(roomId, threadId, editContent);
|
this.props.mxClient.sendMessage(roomId, threadId, editContent);
|
||||||
dis.dispatch({ action: "message_sent" });
|
dis.dispatch({ action: "message_sent" });
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,8 @@ import {
|
||||||
startNewVoiceBroadcastRecording,
|
startNewVoiceBroadcastRecording,
|
||||||
VoiceBroadcastRecordingsStore,
|
VoiceBroadcastRecordingsStore,
|
||||||
} from '../../../voice-broadcast';
|
} from '../../../voice-broadcast';
|
||||||
import { WysiwygComposer } from './wysiwyg_composer/WysiwygComposer';
|
import { SendWysiwygComposer, sendMessage } from './wysiwyg_composer/';
|
||||||
|
import { MatrixClientProps, withMatrixClientHOC } from '../../../contexts/MatrixClientContext';
|
||||||
|
|
||||||
let instanceCount = 0;
|
let instanceCount = 0;
|
||||||
|
|
||||||
|
@ -77,7 +78,7 @@ function SendButton(props: ISendButtonProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps extends MatrixClientProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
permalinkCreator: RoomPermalinkCreator;
|
permalinkCreator: RoomPermalinkCreator;
|
||||||
|
@ -89,6 +90,7 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
composerContent: string;
|
||||||
isComposerEmpty: boolean;
|
isComposerEmpty: boolean;
|
||||||
haveRecording: boolean;
|
haveRecording: boolean;
|
||||||
recordingTimeLeftSeconds?: number;
|
recordingTimeLeftSeconds?: number;
|
||||||
|
@ -100,13 +102,12 @@ interface IState {
|
||||||
showVoiceBroadcastButton: boolean;
|
showVoiceBroadcastButton: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class MessageComposer extends React.Component<IProps, IState> {
|
class MessageComposer extends React.Component<IProps, IState> {
|
||||||
private dispatcherRef?: string;
|
private dispatcherRef?: string;
|
||||||
private messageComposerInput = createRef<SendMessageComposerClass>();
|
private messageComposerInput = createRef<SendMessageComposerClass>();
|
||||||
private voiceRecordingButton = createRef<VoiceRecordComposerTile>();
|
private voiceRecordingButton = createRef<VoiceRecordComposerTile>();
|
||||||
private ref: React.RefObject<HTMLDivElement> = createRef();
|
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||||
private instanceId: number;
|
private instanceId: number;
|
||||||
private composerSendMessage?: () => void;
|
|
||||||
|
|
||||||
private _voiceRecording: Optional<VoiceMessageRecording>;
|
private _voiceRecording: Optional<VoiceMessageRecording>;
|
||||||
|
|
||||||
|
@ -124,6 +125,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isComposerEmpty: true,
|
isComposerEmpty: true,
|
||||||
|
composerContent: '',
|
||||||
haveRecording: false,
|
haveRecording: false,
|
||||||
recordingTimeLeftSeconds: undefined, // when set to a number, shows a toast
|
recordingTimeLeftSeconds: undefined, // when set to a number, shows a toast
|
||||||
isMenuOpen: false,
|
isMenuOpen: false,
|
||||||
|
@ -315,7 +317,15 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.messageComposerInput.current?.sendMessage();
|
this.messageComposerInput.current?.sendMessage();
|
||||||
this.composerSendMessage?.();
|
// this.composerSendMessage?.();
|
||||||
|
const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer");
|
||||||
|
|
||||||
|
if (isWysiwygComposerEnabled) {
|
||||||
|
const { permalinkCreator, relation, replyToEvent } = this.props;
|
||||||
|
sendMessage(this.state.composerContent,
|
||||||
|
{ mxClient: this.props.mxClient, roomContext: this.context, permalinkCreator, relation, replyToEvent });
|
||||||
|
dis.dispatch({ action: Action.ClearAndFocusSendMessageComposer });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onChange = (model: EditorModel) => {
|
private onChange = (model: EditorModel) => {
|
||||||
|
@ -326,6 +336,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private onWysiwygChange = (content: string) => {
|
private onWysiwygChange = (content: string) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
composerContent: content,
|
||||||
isComposerEmpty: content?.length === 0,
|
isComposerEmpty: content?.length === 0,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -406,16 +417,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
if (canSendMessages) {
|
if (canSendMessages) {
|
||||||
if (isWysiwygComposerEnabled) {
|
if (isWysiwygComposerEnabled) {
|
||||||
controls.push(
|
controls.push(
|
||||||
<WysiwygComposer key="controls_input"
|
<SendWysiwygComposer key="controls_input"
|
||||||
disabled={this.state.haveRecording}
|
disabled={this.state.haveRecording}
|
||||||
onChange={this.onWysiwygChange}
|
onChange={this.onWysiwygChange}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
/>,
|
||||||
relation={this.props.relation}
|
|
||||||
replyToEvent={this.props.replyToEvent}>
|
|
||||||
{ (sendMessage) => {
|
|
||||||
this.composerSendMessage = sendMessage;
|
|
||||||
} }
|
|
||||||
</WysiwygComposer>,
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
controls.push(
|
controls.push(
|
||||||
|
@ -555,3 +560,6 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MessageComposerWithMatrixClient = withMatrixClientHOC(MessageComposer);
|
||||||
|
export default MessageComposerWithMatrixClient;
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
/*
|
||||||
|
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, { forwardRef, RefObject, useMemo } from 'react';
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { useRoomContext } from '../../../../contexts/RoomContext';
|
||||||
|
import { useMatrixClientContext } from '../../../../contexts/MatrixClientContext';
|
||||||
|
import EditorStateTransfer from '../../../../utils/EditorStateTransfer';
|
||||||
|
import { CommandPartCreator, Part } from '../../../../editor/parts';
|
||||||
|
import { IRoomState } from '../../../structures/RoomView';
|
||||||
|
import SettingsStore from '../../../../settings/SettingsStore';
|
||||||
|
import { parseEvent } from '../../../../editor/deserialize';
|
||||||
|
import { WysiwygComposer } from './components/WysiwygComposer';
|
||||||
|
import { EditionButtons } from './components/EditionButtons';
|
||||||
|
import { useWysiwygEditActionHandler } from './hooks/useWysiwygEditActionHandler';
|
||||||
|
import { endEditing } from './utils/editing';
|
||||||
|
import { editMessage } from './utils/message';
|
||||||
|
|
||||||
|
function parseEditorStateTransfer(
|
||||||
|
editorStateTransfer: EditorStateTransfer,
|
||||||
|
roomContext: IRoomState,
|
||||||
|
mxClient: MatrixClient,
|
||||||
|
) {
|
||||||
|
if (!roomContext.room) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { room } = roomContext;
|
||||||
|
|
||||||
|
const partCreator = new CommandPartCreator(room, mxClient);
|
||||||
|
|
||||||
|
let parts: Part[];
|
||||||
|
if (editorStateTransfer.hasEditorState()) {
|
||||||
|
// if restoring state from a previous editor,
|
||||||
|
// restore serialized parts from the state
|
||||||
|
parts = editorStateTransfer.getSerializedParts().map(p => partCreator.deserializePart(p));
|
||||||
|
} else {
|
||||||
|
// otherwise, either restore serialized parts from localStorage or parse the body of the event
|
||||||
|
// TODO local storage
|
||||||
|
// const restoredParts = this.restoreStoredEditorState(partCreator);
|
||||||
|
|
||||||
|
if (editorStateTransfer.getEvent().getContent().format === 'org.matrix.custom.html') {
|
||||||
|
return editorStateTransfer.getEvent().getContent().formatted_body || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
parts = parseEvent(editorStateTransfer.getEvent(), partCreator, {
|
||||||
|
shouldEscape: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.reduce((content, part) => content + part.text, '');
|
||||||
|
// Todo local storage
|
||||||
|
// this.saveStoredEditorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContentProps {
|
||||||
|
disabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Content = forwardRef<HTMLElement, ContentProps>(
|
||||||
|
function Content({ disabled }: ContentProps, forwardRef: RefObject<HTMLElement>) {
|
||||||
|
useWysiwygEditActionHandler(disabled, forwardRef);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
interface EditWysiwygComposerProps {
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange?: (content: string) => void;
|
||||||
|
editorStateTransfer?: EditorStateTransfer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditWysiwygComposer({ editorStateTransfer, ...props }: EditWysiwygComposerProps) {
|
||||||
|
const roomContext = useRoomContext();
|
||||||
|
const mxClient = useMatrixClientContext();
|
||||||
|
|
||||||
|
const initialContent = useMemo(() => {
|
||||||
|
if (editorStateTransfer) {
|
||||||
|
return parseEditorStateTransfer(editorStateTransfer, roomContext, mxClient);
|
||||||
|
}
|
||||||
|
}, [editorStateTransfer, roomContext, mxClient]);
|
||||||
|
const isReady = !editorStateTransfer || Boolean(initialContent);
|
||||||
|
|
||||||
|
return isReady && <WysiwygComposer initialContent={initialContent} {...props}>{ (ref, wysiwyg, content) => (
|
||||||
|
<>
|
||||||
|
<Content disabled={props.disabled} ref={ref} />
|
||||||
|
<EditionButtons onCancelClick={() => endEditing(roomContext)} onSaveClick={() => editMessage(content, { roomContext, mxClient, editorStateTransfer })} />
|
||||||
|
</>)
|
||||||
|
}
|
||||||
|
</WysiwygComposer>;
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
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, { forwardRef, RefObject } from 'react';
|
||||||
|
|
||||||
|
import { useWysiwygSendActionHandler } from './hooks/useWysiwygSendActionHandler';
|
||||||
|
import { WysiwygComposer } from './components/WysiwygComposer';
|
||||||
|
import { Wysiwyg } from './types';
|
||||||
|
|
||||||
|
interface SendWysiwygComposerProps {
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange?: (content: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SendWysiwygComposer(props: SendWysiwygComposerProps) {
|
||||||
|
return (
|
||||||
|
<WysiwygComposer {...props}>{ (ref, wysiwyg) => (
|
||||||
|
<Content disabled={props.disabled} ref={ref} wysiwyg={wysiwyg} />
|
||||||
|
) }
|
||||||
|
</WysiwygComposer>);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContentProps {
|
||||||
|
disabled: boolean;
|
||||||
|
wysiwyg: Wysiwyg;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Content = forwardRef<HTMLElement, ContentProps>(
|
||||||
|
function Content({ disabled, wysiwyg }: ContentProps, forwardRef: RefObject<HTMLElement>) {
|
||||||
|
useWysiwygSendActionHandler(disabled, forwardRef, wysiwyg);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
|
@ -1,68 +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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useCallback, useEffect } from 'react';
|
|
||||||
import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
|
||||||
import { useWysiwyg } from "@matrix-org/matrix-wysiwyg";
|
|
||||||
|
|
||||||
import { Editor } from './Editor';
|
|
||||||
import { FormattingButtons } from './FormattingButtons';
|
|
||||||
import { RoomPermalinkCreator } from '../../../../utils/permalinks/Permalinks';
|
|
||||||
import { sendMessage } from './message';
|
|
||||||
import { useMatrixClientContext } from '../../../../contexts/MatrixClientContext';
|
|
||||||
import { useRoomContext } from '../../../../contexts/RoomContext';
|
|
||||||
import { useWysiwygActionHandler } from './useWysiwygActionHandler';
|
|
||||||
|
|
||||||
interface WysiwygProps {
|
|
||||||
disabled?: boolean;
|
|
||||||
onChange: (content: string) => void;
|
|
||||||
relation?: IEventRelation;
|
|
||||||
replyToEvent?: MatrixEvent;
|
|
||||||
permalinkCreator: RoomPermalinkCreator;
|
|
||||||
includeReplyLegacyFallback?: boolean;
|
|
||||||
children?: (sendMessage: () => void) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function WysiwygComposer(
|
|
||||||
{ disabled = false, onChange, children, ...props }: WysiwygProps,
|
|
||||||
) {
|
|
||||||
const roomContext = useRoomContext();
|
|
||||||
const mxClient = useMatrixClientContext();
|
|
||||||
|
|
||||||
const { ref, isWysiwygReady, content, formattingStates, wysiwyg } = useWysiwyg();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!disabled && content !== null) {
|
|
||||||
onChange(content);
|
|
||||||
}
|
|
||||||
}, [onChange, content, disabled]);
|
|
||||||
|
|
||||||
const memoizedSendMessage = useCallback(() => {
|
|
||||||
sendMessage(content, { mxClient, roomContext, ...props });
|
|
||||||
wysiwyg.clear();
|
|
||||||
ref.current?.focus();
|
|
||||||
}, [content, mxClient, roomContext, wysiwyg, props, ref]);
|
|
||||||
|
|
||||||
useWysiwygActionHandler(disabled, ref);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx_WysiwygComposer">
|
|
||||||
<FormattingButtons composer={wysiwyg} formattingStates={formattingStates} />
|
|
||||||
<Editor ref={ref} disabled={!isWysiwygReady || disabled} />
|
|
||||||
{ children?.(memoizedSendMessage) }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
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, { MouseEventHandler } from 'react';
|
||||||
|
|
||||||
|
import { _t } from '../../../../../languageHandler';
|
||||||
|
import AccessibleButton from '../../../elements/AccessibleButton';
|
||||||
|
|
||||||
|
interface EditionButtonsProps {
|
||||||
|
onCancelClick: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
onSaveClick: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditionButtons({ onCancelClick, onSaveClick }: EditionButtonsProps) {
|
||||||
|
return <div className="mx_EditMessageComposer_buttons">
|
||||||
|
<AccessibleButton kind="secondary" onClick={onCancelClick}>
|
||||||
|
{ _t("Cancel") }
|
||||||
|
</AccessibleButton>
|
||||||
|
<AccessibleButton kind="primary" disabled={false} onClick={onSaveClick}>
|
||||||
|
{ _t("Save") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>;
|
||||||
|
}
|
|
@ -18,11 +18,12 @@ import React, { MouseEventHandler } from "react";
|
||||||
import { useWysiwyg } from "@matrix-org/matrix-wysiwyg";
|
import { useWysiwyg } from "@matrix-org/matrix-wysiwyg";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import AccessibleTooltipButton from "../../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../../../elements/AccessibleTooltipButton";
|
||||||
import { Alignment } from "../../elements/Tooltip";
|
import { Alignment } from "../../../elements/Tooltip";
|
||||||
import { KeyboardShortcut } from "../../settings/KeyboardShortcut";
|
import { KeyboardShortcut } from "../../../settings/KeyboardShortcut";
|
||||||
import { KeyCombo } from "../../../../KeyBindingsManager";
|
import { KeyCombo } from "../../../../../KeyBindingsManager";
|
||||||
import { _td } from "../../../../languageHandler";
|
import { _td } from "../../../../../languageHandler";
|
||||||
|
import { Wysiwyg } from "../types";
|
||||||
|
|
||||||
interface TooltipProps {
|
interface TooltipProps {
|
||||||
label: string;
|
label: string;
|
||||||
|
@ -55,7 +56,7 @@ function Button({ label, keyCombo, onClick, isActive, className }: ButtonProps)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FormattingButtonsProps {
|
interface FormattingButtonsProps {
|
||||||
composer: ReturnType<typeof useWysiwyg>['wysiwyg'];
|
composer: Wysiwyg;
|
||||||
formattingStates: ReturnType<typeof useWysiwyg>['formattingStates'];
|
formattingStates: ReturnType<typeof useWysiwyg>['formattingStates'];
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
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, useEffect } from 'react';
|
||||||
|
import { useWysiwyg } from "@matrix-org/matrix-wysiwyg";
|
||||||
|
|
||||||
|
import { FormattingButtons } from './FormattingButtons';
|
||||||
|
import { Editor } from './Editor';
|
||||||
|
import { Wysiwyg } from '../types';
|
||||||
|
|
||||||
|
interface WysiwygComposerProps {
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange?: (content: string) => void;
|
||||||
|
initialContent?: string;
|
||||||
|
children?: (ref: MutableRefObject<HTMLDivElement | null>, wysiwyg: Wysiwyg, content: string) => ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WysiwygComposer(
|
||||||
|
{ disabled = false, onChange, initialContent, children }: WysiwygComposerProps,
|
||||||
|
) {
|
||||||
|
const { ref, isWysiwygReady, content, formattingStates, wysiwyg } = useWysiwyg({ initialContent });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!disabled && content !== null) {
|
||||||
|
onChange?.(content);
|
||||||
|
}
|
||||||
|
}, [onChange, content, disabled]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_WysiwygComposer">
|
||||||
|
<FormattingButtons composer={wysiwyg} formattingStates={formattingStates} />
|
||||||
|
<Editor ref={ref} disabled={!isWysiwygReady || disabled} />
|
||||||
|
{ children?.(ref, wysiwyg, content) }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
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, useCallback, useRef } from "react";
|
||||||
|
|
||||||
|
import defaultDispatcher from "../../../../../dispatcher/dispatcher";
|
||||||
|
import { Action } from "../../../../../dispatcher/actions";
|
||||||
|
import { ActionPayload } from "../../../../../dispatcher/payloads";
|
||||||
|
import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/RoomContext";
|
||||||
|
import { useDispatcher } from "../../../../../hooks/useDispatcher";
|
||||||
|
import { focusComposer } from "./utils";
|
||||||
|
|
||||||
|
export function useWysiwygEditActionHandler(
|
||||||
|
disabled: boolean,
|
||||||
|
composerElement: RefObject<HTMLElement>,
|
||||||
|
) {
|
||||||
|
const roomContext = useRoomContext();
|
||||||
|
const timeoutId = useRef<number>();
|
||||||
|
|
||||||
|
const handler = useCallback((payload: ActionPayload) => {
|
||||||
|
// don't let the user into the composer if it is disabled - all of these branches lead
|
||||||
|
// to the cursor being in the composer
|
||||||
|
if (disabled || !composerElement.current) return;
|
||||||
|
|
||||||
|
const context = payload.context ?? TimelineRenderingType.Room;
|
||||||
|
|
||||||
|
switch (payload.action) {
|
||||||
|
case Action.FocusSendMessageComposer:
|
||||||
|
focusComposer(composerElement, context, roomContext, timeoutId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, [disabled, composerElement, timeoutId, roomContext]);
|
||||||
|
|
||||||
|
useDispatcher(defaultDispatcher, handler);
|
||||||
|
}
|
|
@ -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 { RefObject, useCallback, useRef } from "react";
|
||||||
|
|
||||||
|
import defaultDispatcher from "../../../../../dispatcher/dispatcher";
|
||||||
|
import { Action } from "../../../../../dispatcher/actions";
|
||||||
|
import { ActionPayload } from "../../../../../dispatcher/payloads";
|
||||||
|
import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/RoomContext";
|
||||||
|
import { useDispatcher } from "../../../../../hooks/useDispatcher";
|
||||||
|
import { Wysiwyg } from "../types";
|
||||||
|
import { focusComposer } from "./utils";
|
||||||
|
|
||||||
|
export function useWysiwygSendActionHandler(
|
||||||
|
disabled: boolean,
|
||||||
|
composerElement: RefObject<HTMLElement>,
|
||||||
|
wysiwyg: Wysiwyg,
|
||||||
|
) {
|
||||||
|
const roomContext = useRoomContext();
|
||||||
|
const timeoutId = useRef<number>();
|
||||||
|
|
||||||
|
const handler = useCallback((payload: ActionPayload) => {
|
||||||
|
// don't let the user into the composer if it is disabled - all of these branches lead
|
||||||
|
// to the cursor being in the composer
|
||||||
|
if (disabled || !composerElement.current) return;
|
||||||
|
|
||||||
|
const context = payload.context ?? TimelineRenderingType.Room;
|
||||||
|
|
||||||
|
switch (payload.action) {
|
||||||
|
case "reply_to_event":
|
||||||
|
case Action.FocusSendMessageComposer:
|
||||||
|
focusComposer(composerElement, context, roomContext, timeoutId);
|
||||||
|
break;
|
||||||
|
case Action.ClearAndFocusSendMessageComposer:
|
||||||
|
wysiwyg.clear();
|
||||||
|
focusComposer(composerElement, context, roomContext, timeoutId);
|
||||||
|
break;
|
||||||
|
// TODO: case Action.ComposerInsert: - see SendMessageComposer
|
||||||
|
}
|
||||||
|
}, [disabled, composerElement, wysiwyg, timeoutId, roomContext]);
|
||||||
|
|
||||||
|
useDispatcher(defaultDispatcher, handler);
|
||||||
|
}
|
|
@ -14,40 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useRef } from "react";
|
import { TimelineRenderingType } from "../../../../../contexts/RoomContext";
|
||||||
|
import { IRoomState } from "../../../../structures/RoomView";
|
||||||
|
|
||||||
import defaultDispatcher from "../../../../dispatcher/dispatcher";
|
export function focusComposer(
|
||||||
import { Action } from "../../../../dispatcher/actions";
|
|
||||||
import { ActionPayload } from "../../../../dispatcher/payloads";
|
|
||||||
import { IRoomState } from "../../../structures/RoomView";
|
|
||||||
import { TimelineRenderingType, useRoomContext } from "../../../../contexts/RoomContext";
|
|
||||||
import { useDispatcher } from "../../../../hooks/useDispatcher";
|
|
||||||
|
|
||||||
export function useWysiwygActionHandler(
|
|
||||||
disabled: boolean,
|
|
||||||
composerElement: React.MutableRefObject<HTMLElement>,
|
|
||||||
) {
|
|
||||||
const roomContext = useRoomContext();
|
|
||||||
const timeoutId = useRef<number>();
|
|
||||||
|
|
||||||
useDispatcher(defaultDispatcher, (payload: ActionPayload) => {
|
|
||||||
// don't let the user into the composer if it is disabled - all of these branches lead
|
|
||||||
// to the cursor being in the composer
|
|
||||||
if (disabled) return;
|
|
||||||
|
|
||||||
const context = payload.context ?? TimelineRenderingType.Room;
|
|
||||||
|
|
||||||
switch (payload.action) {
|
|
||||||
case "reply_to_event":
|
|
||||||
case Action.FocusSendMessageComposer:
|
|
||||||
focusComposer(composerElement, context, roomContext, timeoutId);
|
|
||||||
break;
|
|
||||||
// TODO: case Action.ComposerInsert: - see SendMessageComposer
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function focusComposer(
|
|
||||||
composerElement: React.MutableRefObject<HTMLElement>,
|
composerElement: React.MutableRefObject<HTMLElement>,
|
||||||
renderingType: TimelineRenderingType,
|
renderingType: TimelineRenderingType,
|
||||||
roomContext: IRoomState,
|
roomContext: IRoomState,
|
|
@ -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 { SendWysiwygComposer } from './SendWysiwygComposer';
|
||||||
|
export { EditWysiwygComposer } from './EditWysiwygComposer';
|
||||||
|
export { sendMessage } from './utils/message';
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
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 { useWysiwyg } from "@matrix-org/matrix-wysiwyg";
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// Change when the matrix-wysiwyg typescript definition will be refined
|
||||||
|
export type Wysiwyg = ReturnType<typeof useWysiwyg>['wysiwyg'];
|
|
@ -0,0 +1,117 @@
|
||||||
|
/*
|
||||||
|
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 { IContent, IEventRelation, MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { RoomPermalinkCreator } from "../../../../../utils/permalinks/Permalinks";
|
||||||
|
import { addReplyToMessageContent } from "../../../../../utils/Reply";
|
||||||
|
|
||||||
|
// Merges favouring the given relation
|
||||||
|
function attachRelation(content: IContent, relation?: IEventRelation): void {
|
||||||
|
if (relation) {
|
||||||
|
content['m.relates_to'] = {
|
||||||
|
...(content['m.relates_to'] || {}),
|
||||||
|
...relation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
||||||
|
const html = mxEvent.getContent().formatted_body;
|
||||||
|
if (!html) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const rootNode = new DOMParser().parseFromString(html, "text/html").body;
|
||||||
|
const mxReply = rootNode.querySelector("mx-reply");
|
||||||
|
return (mxReply && mxReply.outerHTML) || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateMessageContentParams {
|
||||||
|
relation?: IEventRelation;
|
||||||
|
replyToEvent?: MatrixEvent;
|
||||||
|
permalinkCreator?: RoomPermalinkCreator;
|
||||||
|
includeReplyLegacyFallback?: boolean;
|
||||||
|
editedEvent?: MatrixEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMessageContent(
|
||||||
|
message: string,
|
||||||
|
{ relation, replyToEvent, permalinkCreator, includeReplyLegacyFallback = true, editedEvent }:
|
||||||
|
CreateMessageContentParams,
|
||||||
|
): IContent {
|
||||||
|
// TODO emote ?
|
||||||
|
|
||||||
|
const isReply = Boolean(replyToEvent?.replyEventId);
|
||||||
|
const isEditing = Boolean(editedEvent);
|
||||||
|
|
||||||
|
/*const isEmote = containsEmote(model);
|
||||||
|
if (isEmote) {
|
||||||
|
model = stripEmoteCommand(model);
|
||||||
|
}
|
||||||
|
if (startsWith(model, "//")) {
|
||||||
|
model = stripPrefix(model, "/");
|
||||||
|
}
|
||||||
|
model = unescapeMessage(model);*/
|
||||||
|
|
||||||
|
// const body = textSerialize(model);
|
||||||
|
const body = message;
|
||||||
|
|
||||||
|
const content: IContent = {
|
||||||
|
// TODO emote
|
||||||
|
// msgtype: isEmote ? "m.emote" : "m.text",
|
||||||
|
msgtype: MsgType.Text,
|
||||||
|
body: body,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO markdown support
|
||||||
|
|
||||||
|
/*const formattedBody = htmlSerializeIfNeeded(model, {
|
||||||
|
forceHTML: !!replyToEvent,
|
||||||
|
useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
|
||||||
|
});*/
|
||||||
|
const formattedBody = message;
|
||||||
|
|
||||||
|
if (formattedBody) {
|
||||||
|
content.format = "org.matrix.custom.html";
|
||||||
|
|
||||||
|
const htmlPrefix = isReply ? getHtmlReplyFallback(editedEvent) : '';
|
||||||
|
content.formatted_body = isEditing ? `${htmlPrefix} * ${formattedBody}` : formattedBody;
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
content['m.new_content'] = {
|
||||||
|
"msgtype": content.msgtype,
|
||||||
|
"body": body,
|
||||||
|
"format": "org.matrix.custom.html",
|
||||||
|
'formatted_body': formattedBody,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRelation = isEditing ?
|
||||||
|
{ ...relation, 'rel_type': 'm.replace', 'event_id': editedEvent.getId() }
|
||||||
|
: relation;
|
||||||
|
|
||||||
|
attachRelation(content, newRelation);
|
||||||
|
|
||||||
|
if (!isEditing && replyToEvent && permalinkCreator) {
|
||||||
|
addReplyToMessageContent(content, replyToEvent, {
|
||||||
|
permalinkCreator,
|
||||||
|
includeLegacyFallback: includeReplyLegacyFallback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
|
@ -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 { EventStatus, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { IRoomState } from "../../../../structures/RoomView";
|
||||||
|
import dis from '../../../../../dispatcher/dispatcher';
|
||||||
|
import { Action } from "../../../../../dispatcher/actions";
|
||||||
|
import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
|
||||||
|
|
||||||
|
export function endEditing(roomContext: IRoomState) {
|
||||||
|
// todo local storage
|
||||||
|
// localStorage.removeItem(this.editorRoomKey);
|
||||||
|
// localStorage.removeItem(this.editorStateKey);
|
||||||
|
|
||||||
|
// close the event editing and focus composer
|
||||||
|
dis.dispatch({
|
||||||
|
action: Action.EditEvent,
|
||||||
|
event: null,
|
||||||
|
timelineRenderingType: roomContext.timelineRenderingType,
|
||||||
|
});
|
||||||
|
dis.dispatch({
|
||||||
|
action: Action.FocusSendMessageComposer,
|
||||||
|
context: roomContext.timelineRenderingType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancelPreviousPendingEdit(mxClient: MatrixClient, editorStateTransfer: EditorStateTransfer) {
|
||||||
|
const originalEvent = editorStateTransfer.getEvent();
|
||||||
|
const previousEdit = originalEvent.replacingEvent();
|
||||||
|
if (previousEdit && (
|
||||||
|
previousEdit.status === EventStatus.QUEUED ||
|
||||||
|
previousEdit.status === EventStatus.NOT_SENT
|
||||||
|
)) {
|
||||||
|
mxClient.cancelPendingEvent(previousEdit);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
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 { IContent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
|
||||||
|
|
||||||
|
export function isContentModified(newContent: IContent, editorStateTransfer: EditorStateTransfer): boolean {
|
||||||
|
// if nothing has changed then bail
|
||||||
|
const oldContent = editorStateTransfer.getEvent().getContent();
|
||||||
|
if (oldContent["msgtype"] === newContent["msgtype"] && oldContent["body"] === newContent["body"] &&
|
||||||
|
oldContent["format"] === newContent["format"] &&
|
||||||
|
oldContent["formatted_body"] === newContent["formatted_body"]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
|
@ -19,90 +19,32 @@ import { IContent, IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { 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";
|
||||||
|
|
||||||
import { PosthogAnalytics } from "../../../../PosthogAnalytics";
|
import { PosthogAnalytics } from "../../../../../PosthogAnalytics";
|
||||||
import SettingsStore from "../../../../settings/SettingsStore";
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../../sendTimePerformanceMetrics";
|
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../../../sendTimePerformanceMetrics";
|
||||||
import { RoomPermalinkCreator } from "../../../../utils/permalinks/Permalinks";
|
import { RoomPermalinkCreator } from "../../../../../utils/permalinks/Permalinks";
|
||||||
import { doMaybeLocalRoomAction } from "../../../../utils/local-room";
|
import { doMaybeLocalRoomAction } from "../../../../../utils/local-room";
|
||||||
import { CHAT_EFFECTS } from "../../../../effects";
|
import { CHAT_EFFECTS } from "../../../../../effects";
|
||||||
import { containsEmoji } from "../../../../effects/utils";
|
import { containsEmoji } from "../../../../../effects/utils";
|
||||||
import { IRoomState } from "../../../structures/RoomView";
|
import { IRoomState } from "../../../../structures/RoomView";
|
||||||
import dis from '../../../../dispatcher/dispatcher';
|
import dis from '../../../../../dispatcher/dispatcher';
|
||||||
import { addReplyToMessageContent } from "../../../../utils/Reply";
|
import { createRedactEventDialog } from "../../../dialogs/ConfirmRedactDialog";
|
||||||
|
import { endEditing, cancelPreviousPendingEdit } from "./editing";
|
||||||
// Merges favouring the given relation
|
import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
|
||||||
function attachRelation(content: IContent, relation?: IEventRelation): void {
|
import { createMessageContent } from "./createMessageContent";
|
||||||
if (relation) {
|
import { isContentModified } from "./isContentModified";
|
||||||
content['m.relates_to'] = {
|
|
||||||
...(content['m.relates_to'] || {}),
|
|
||||||
...relation,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SendMessageParams {
|
interface SendMessageParams {
|
||||||
mxClient: MatrixClient;
|
mxClient: MatrixClient;
|
||||||
relation?: IEventRelation;
|
relation?: IEventRelation;
|
||||||
replyToEvent?: MatrixEvent;
|
replyToEvent?: MatrixEvent;
|
||||||
roomContext: IRoomState;
|
roomContext: IRoomState;
|
||||||
permalinkCreator: RoomPermalinkCreator;
|
permalinkCreator?: RoomPermalinkCreator;
|
||||||
includeReplyLegacyFallback?: boolean;
|
includeReplyLegacyFallback?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// exported for tests
|
|
||||||
export function createMessageContent(
|
|
||||||
message: string,
|
|
||||||
{ relation, replyToEvent, permalinkCreator, includeReplyLegacyFallback = true }:
|
|
||||||
Omit<SendMessageParams, 'roomContext' | 'mxClient'>,
|
|
||||||
): IContent {
|
|
||||||
// TODO emote ?
|
|
||||||
|
|
||||||
/*const isEmote = containsEmote(model);
|
|
||||||
if (isEmote) {
|
|
||||||
model = stripEmoteCommand(model);
|
|
||||||
}
|
|
||||||
if (startsWith(model, "//")) {
|
|
||||||
model = stripPrefix(model, "/");
|
|
||||||
}
|
|
||||||
model = unescapeMessage(model);*/
|
|
||||||
|
|
||||||
// const body = textSerialize(model);
|
|
||||||
const body = message;
|
|
||||||
|
|
||||||
const content: IContent = {
|
|
||||||
// TODO emote
|
|
||||||
// msgtype: isEmote ? "m.emote" : "m.text",
|
|
||||||
msgtype: "m.text",
|
|
||||||
body: body,
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO markdown support
|
|
||||||
|
|
||||||
/*const formattedBody = htmlSerializeIfNeeded(model, {
|
|
||||||
forceHTML: !!replyToEvent,
|
|
||||||
useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
|
|
||||||
});*/
|
|
||||||
const formattedBody = message;
|
|
||||||
|
|
||||||
if (formattedBody) {
|
|
||||||
content.format = "org.matrix.custom.html";
|
|
||||||
content.formatted_body = formattedBody;
|
|
||||||
}
|
|
||||||
|
|
||||||
attachRelation(content, relation);
|
|
||||||
|
|
||||||
if (replyToEvent) {
|
|
||||||
addReplyToMessageContent(content, replyToEvent, {
|
|
||||||
permalinkCreator,
|
|
||||||
includeLegacyFallback: includeReplyLegacyFallback,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sendMessage(
|
export function sendMessage(
|
||||||
message: string,
|
html: string,
|
||||||
{ roomContext, mxClient, ...params }: SendMessageParams,
|
{ roomContext, mxClient, ...params }: SendMessageParams,
|
||||||
) {
|
) {
|
||||||
const { relation, replyToEvent } = params;
|
const { relation, replyToEvent } = params;
|
||||||
|
@ -113,6 +55,7 @@ export function sendMessage(
|
||||||
eventName: "Composer",
|
eventName: "Composer",
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
isReply: Boolean(replyToEvent),
|
isReply: Boolean(replyToEvent),
|
||||||
|
// TODO thread
|
||||||
inThread: relation?.rel_type === THREAD_RELATION_TYPE.name,
|
inThread: relation?.rel_type === THREAD_RELATION_TYPE.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -133,7 +76,7 @@ export function sendMessage(
|
||||||
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
content = createMessageContent(
|
content = createMessageContent(
|
||||||
message,
|
html,
|
||||||
params,
|
params,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -197,3 +140,65 @@ export function sendMessage(
|
||||||
|
|
||||||
return prom;
|
return prom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface EditMessageParams {
|
||||||
|
mxClient: MatrixClient;
|
||||||
|
roomContext: IRoomState;
|
||||||
|
editorStateTransfer: EditorStateTransfer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editMessage(
|
||||||
|
html: string,
|
||||||
|
{ roomContext, mxClient, editorStateTransfer }: EditMessageParams,
|
||||||
|
) {
|
||||||
|
const editedEvent = editorStateTransfer.getEvent();
|
||||||
|
|
||||||
|
PosthogAnalytics.instance.trackEvent<ComposerEvent>({
|
||||||
|
eventName: "Composer",
|
||||||
|
isEditing: true,
|
||||||
|
inThread: Boolean(editedEvent?.getThread()),
|
||||||
|
isReply: Boolean(editedEvent.replyEventId),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Replace emoticon at the end of the message
|
||||||
|
/* if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
|
||||||
|
const caret = this.editorRef.current?.getCaret();
|
||||||
|
const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||||
|
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
|
||||||
|
}*/
|
||||||
|
const editContent = createMessageContent(html, { editedEvent });
|
||||||
|
const newContent = editContent["m.new_content"];
|
||||||
|
|
||||||
|
const shouldSend = true;
|
||||||
|
|
||||||
|
if (newContent?.body === '') {
|
||||||
|
cancelPreviousPendingEdit(mxClient, editorStateTransfer);
|
||||||
|
createRedactEventDialog({
|
||||||
|
mxEvent: editedEvent,
|
||||||
|
onCloseDialog: () => {
|
||||||
|
endEditing(roomContext);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If content is modified then send an updated event into the room
|
||||||
|
if (isContentModified(newContent, editorStateTransfer)) {
|
||||||
|
const roomId = editedEvent.getRoomId();
|
||||||
|
|
||||||
|
// TODO Slash Commands
|
||||||
|
|
||||||
|
if (shouldSend) {
|
||||||
|
cancelPreviousPendingEdit(mxClient, editorStateTransfer);
|
||||||
|
|
||||||
|
const event = editorStateTransfer.getEvent();
|
||||||
|
const threadId = event.threadRootId || null;
|
||||||
|
|
||||||
|
console.log('editContent', editContent);
|
||||||
|
mxClient.sendMessage(roomId, threadId, editContent);
|
||||||
|
dis.dispatch({ action: "message_sent" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endEditing(roomContext);
|
||||||
|
}
|
|
@ -75,6 +75,11 @@ export enum Action {
|
||||||
*/
|
*/
|
||||||
FocusSendMessageComposer = "focus_send_message_composer",
|
FocusSendMessageComposer = "focus_send_message_composer",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the to the send message composer. Should be used with a FocusComposerPayload.
|
||||||
|
*/
|
||||||
|
ClearAndFocusSendMessageComposer = "clear_focus_send_message_composer",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Focuses the user's cursor to the edit message composer. Should be used with a FocusComposerPayload.
|
* Focuses the user's cursor to the edit message composer. Should be used with a FocusComposerPayload.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -39,7 +39,7 @@ import { SendMessageComposer } from "../../../../src/components/views/rooms/Send
|
||||||
import { E2EStatus } from "../../../../src/utils/ShieldUtils";
|
import { E2EStatus } from "../../../../src/utils/ShieldUtils";
|
||||||
import { addTextToComposer } from "../../../test-utils/composer";
|
import { addTextToComposer } from "../../../test-utils/composer";
|
||||||
import UIStore, { UI_EVENTS } from "../../../../src/stores/UIStore";
|
import UIStore, { UI_EVENTS } from "../../../../src/stores/UIStore";
|
||||||
import { WysiwygComposer } from "../../../../src/components/views/rooms/wysiwyg_composer/WysiwygComposer";
|
import { WysiwygComposer } from "../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer";
|
||||||
|
|
||||||
// The wysiwyg fetch wasm bytes and a specific workaround is needed to make it works in a node (jest) environnement
|
// The wysiwyg fetch wasm bytes and a specific workaround is needed to make it works in a node (jest) environnement
|
||||||
// See https://github.com/matrix-org/matrix-wysiwyg/blob/main/platforms/web/test.setup.ts
|
// See https://github.com/matrix-org/matrix-wysiwyg/blob/main/platforms/web/test.setup.ts
|
||||||
|
|
|
@ -18,7 +18,7 @@ import React from 'react';
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
import { FormattingButtons } from "../../../../../src/components/views/rooms/wysiwyg_composer/FormattingButtons";
|
import { FormattingButtons } from "../../../../../src/components/views/rooms/wysiwyg_composer/components/FormattingButtons";
|
||||||
|
|
||||||
describe('FormattingButtons', () => {
|
describe('FormattingButtons', () => {
|
||||||
const wysiwyg = {
|
const wysiwyg = {
|
||||||
|
|
|
@ -24,7 +24,7 @@ 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 { Layout } from "../../../../../src/settings/enums/Layout";
|
import { Layout } from "../../../../../src/settings/enums/Layout";
|
||||||
import { WysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer/WysiwygComposer";
|
import { WysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer";
|
||||||
import { createTestClient, mkEvent, mkStubRoom } from "../../../../test-utils";
|
import { createTestClient, mkEvent, mkStubRoom } from "../../../../test-utils";
|
||||||
|
|
||||||
// The wysiwyg fetch wasm bytes and a specific workaround is needed to make it works in a node (jest) environnement
|
// The wysiwyg fetch wasm bytes and a specific workaround is needed to make it works in a node (jest) environnement
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IRoomState } from "../../../../../src/components/structures/RoomView";
|
import { IRoomState } from "../../../../../src/components/structures/RoomView";
|
||||||
import { createMessageContent, sendMessage } from "../../../../../src/components/views/rooms/wysiwyg_composer/message";
|
import { createMessageContent, sendMessage } from "../../../../../src/components/views/rooms/wysiwyg_composer/utils/message";
|
||||||
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
|
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
|
||||||
import { Layout } from "../../../../../src/settings/enums/Layout";
|
import { Layout } from "../../../../../src/settings/enums/Layout";
|
||||||
import { createTestClient, mkEvent, mkStubRoom } from "../../../../test-utils";
|
import { createTestClient, mkEvent, mkStubRoom } from "../../../../test-utils";
|
||||||
|
|
Loading…
Reference in New Issue