Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Allow image pasting in rich text mode in RTE #11049

Merged
merged 17 commits into from
Jun 9, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,10 @@ export default function SendWysiwygComposer({
isRichTextEnabled,
e2eStatus,
menuPosition,
eventRelation,
...props
}: SendWysiwygComposerProps): JSX.Element {
const Composer = isRichTextEnabled ? WysiwygComposer : PlainTextComposer;
const defaultContextValue = useRef(getDefaultContextValue({ eventRelation }));
Comment on lines -60 to -64
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to allow us to pass the eventRelation prop through to the composer as part of ...props

const defaultContextValue = useRef(getDefaultContextValue({ eventRelation: props.eventRelation }));

return (
<ComposerContext.Provider value={defaultContextValue.current}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ limitations under the License.
*/

import classNames from "classnames";
import { IEventRelation } from "matrix-js-sdk/src/matrix";
import React, { MutableRefObject, ReactNode } from "react";

import { useComposerFunctions } from "../hooks/useComposerFunctions";
Expand All @@ -36,6 +37,7 @@ interface PlainTextComposerProps {
leftComponent?: ReactNode;
rightComponent?: ReactNode;
children?: (ref: MutableRefObject<HTMLDivElement | null>, composerFunctions: ComposerFunctions) => ReactNode;
eventRelation?: IEventRelation;
}

export function PlainTextComposer({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ limitations under the License.
*/

import React, { memo, MutableRefObject, ReactNode, useEffect, useRef } from "react";
import { IEventRelation } from "matrix-js-sdk/src/matrix";
import { useWysiwyg, FormattingFunctions } from "@matrix-org/matrix-wysiwyg";
import classNames from "classnames";

Expand All @@ -40,6 +41,7 @@ interface WysiwygComposerProps {
leftComponent?: ReactNode;
rightComponent?: ReactNode;
children?: (ref: MutableRefObject<HTMLDivElement | null>, wysiwyg: FormattingFunctions) => ReactNode;
eventRelation?: IEventRelation;
}

export const WysiwygComposer = memo(function WysiwygComposer({
Expand All @@ -52,11 +54,12 @@ export const WysiwygComposer = memo(function WysiwygComposer({
leftComponent,
rightComponent,
children,
eventRelation,
}: WysiwygComposerProps) {
const { room } = useRoomContext();
const autocompleteRef = useRef<Autocomplete | null>(null);

const inputEventProcessor = useInputEventProcessor(onSend, autocompleteRef, initialContent);
const inputEventProcessor = useInputEventProcessor(onSend, autocompleteRef, initialContent, eventRelation);
const { ref, isWysiwygReady, content, actionStates, wysiwyg, suggestion } = useWysiwyg({
initialContent,
inputEventProcessor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ limitations under the License.

import { Wysiwyg, WysiwygEvent } from "@matrix-org/matrix-wysiwyg";
import { useCallback } from "react";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { IEventRelation, MatrixClient } from "matrix-js-sdk/src/matrix";

import { useSettingValue } from "../../../../../hooks/useSettings";
import { getKeyBindingsManager } from "../../../../../KeyBindingsManager";
Expand All @@ -34,11 +34,14 @@ import { getEventsFromEditorStateTransfer, getEventsFromRoom } from "../utils/ev
import { endEditing } from "../utils/editing";
import Autocomplete from "../../Autocomplete";
import { handleEventWithAutocomplete } from "./utils";
import ContentMessages from "../../../../../ContentMessages";
import { getBlobSafeMimeType } from "../../../../../utils/blobs";

export function useInputEventProcessor(
onSend: () => void,
autocompleteRef: React.RefObject<Autocomplete>,
initialContent?: string,
eventRelation?: IEventRelation,
): (event: WysiwygEvent, composer: Wysiwyg, editor: HTMLElement) => WysiwygEvent | null {
const roomContext = useRoomContext();
const composerContext = useComposerContext();
Expand All @@ -47,10 +50,6 @@ export function useInputEventProcessor(

return useCallback(
(event: WysiwygEvent, composer: Wysiwyg, editor: HTMLElement) => {
if (event instanceof ClipboardEvent) {
return event;
}

const send = (): void => {
event.stopPropagation?.();
event.preventDefault?.();
Expand All @@ -61,6 +60,12 @@ export function useInputEventProcessor(
onSend();
};

const isClipboardEvent = event instanceof ClipboardEvent;
if (isClipboardEvent) {
const handled = handleClipboardEvent(event, roomContext, mxClient, eventRelation);
return handled ? null : event;
}

const isKeyboardEvent = event instanceof KeyboardEvent;
if (isKeyboardEvent) {
return handleKeyboardEvent(
Expand All @@ -78,7 +83,16 @@ export function useInputEventProcessor(
return handleInputEvent(event, send, isCtrlEnterToSend);
}
},
[isCtrlEnterToSend, onSend, initialContent, roomContext, composerContext, mxClient, autocompleteRef],
[
isCtrlEnterToSend,
onSend,
initialContent,
roomContext,
composerContext,
mxClient,
autocompleteRef,
eventRelation,
],
);
}

Expand Down Expand Up @@ -220,3 +234,86 @@ function handleInputEvent(event: InputEvent, send: Send, isCtrlEnterToSend: bool

return event;
}

/**
* Takes a ClipboardEvent and handles image pasting. Returns a boolean to indicate if it has handled
* the event or not.
*
* @param clipboardEvent - event to process
* @param roomContext - room in which the event occurs
* @param mxClient - current matrix client
* @param eventRelation - used to send the event to the correct timeline
* @returns - boolean to show if the event was handled or not
*/
export function handleClipboardEvent(
clipboardEvent: ClipboardEvent,
roomContext: IRoomState,
mxClient: MatrixClient,
eventRelation?: IEventRelation,
): boolean {
const { clipboardData: data } = clipboardEvent;
const { room, timelineRenderingType, replyToEvent } = roomContext;

function handleError(error: unknown): void {
if (error instanceof Error) {
console.log(error.message);
} else if (typeof error === "string") {
console.log(error);
}
}

if (clipboardEvent.type !== "paste" || data === null || room === undefined) {
return false;
}

// Prioritize text on the clipboard over files if RTF is present as Office on macOS puts a bitmap
// in the clipboard as well as the content being copied. Modern versions of Office seem to not do this anymore.
// We check text/rtf instead of text/plain as when copy+pasting a file from Finder or Gnome Image Viewer
// it puts the filename in as text/plain which we want to ignore.
if (data.files.length && !data.types.includes("text/rtf")) {
ContentMessages.sharedInstance()
.sendContentListToRoom(Array.from(data.files), room.roomId, eventRelation, mxClient, timelineRenderingType)
.catch(handleError);
return true;
}

// Safari `Insert from iPhone or iPad`
// data.getData("text/html") returns a string like: <img src="blob:https://...">
if (data.types.includes("text/html")) {
const imgElementStr = data.getData("text/html");
const parser = new DOMParser();
const imgDoc = parser.parseFromString(imgElementStr, "text/html");
Fixed Show fixed Hide fixed

if (
imgDoc.getElementsByTagName("img").length !== 1 ||
!imgDoc.querySelector("img")?.src.startsWith("blob:") ||
imgDoc.childNodes.length !== 1
) {
handleError("Failed to handle pasted content as Safari inserted content");
return false;
}
const imgSrc = imgDoc.querySelector("img")!.src;

fetch(imgSrc)
.then((response) => {
response
.blob()
.then((imgBlob) => {
const type = imgBlob.type;
const safetype = getBlobSafeMimeType(type);
const ext = type.split("/")[1];
const parts = response.url.split("/");
const filename = parts[parts.length - 1];
const file = new File([imgBlob], filename + "." + ext, { type: safetype });
ContentMessages.sharedInstance()
.sendContentToRoom(file, room.roomId, eventRelation, mxClient, replyToEvent)
.catch(handleError);
})
.catch(handleError);
})
.catch(handleError);
return true;
}

return false;
}
Loading