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

Commit 8abe392

Browse files
Support Insert from iPhone or iPad in Safari (#10851)
Co-authored-by: Michael Telatynski <[email protected]>
1 parent f52fab3 commit 8abe392

File tree

2 files changed

+79
-11
lines changed

2 files changed

+79
-11
lines changed

src/components/views/rooms/BasicMessageComposer.tsx

+23-6
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ limitations under the License.
1515
*/
1616

1717
import classNames from "classnames";
18-
import React, { createRef, ClipboardEvent } from "react";
18+
import React, { createRef, ClipboardEvent, SyntheticEvent } from "react";
1919
import { Room } from "matrix-js-sdk/src/models/room";
2020
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
2121
import EMOTICON_REGEX from "emojibase-regex/emoticon";
@@ -108,7 +108,7 @@ interface IProps {
108108
disabled?: boolean;
109109

110110
onChange?(selection?: Caret, inputType?: string, diff?: IDiff): void;
111-
onPaste?(event: ClipboardEvent<HTMLDivElement>, model: EditorModel): boolean;
111+
onPaste?(event: Event | SyntheticEvent, data: DataTransfer, model: EditorModel): boolean;
112112
}
113113

114114
interface IState {
@@ -355,18 +355,18 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
355355
this.onCutCopy(event, "cut");
356356
};
357357

358-
private onPaste = (event: ClipboardEvent<HTMLDivElement>): boolean | undefined => {
358+
private onPasteHandler = (event: Event | SyntheticEvent, data: DataTransfer): boolean | undefined => {
359359
event.preventDefault(); // we always handle the paste ourselves
360360
if (!this.editorRef.current) return;
361-
if (this.props.onPaste?.(event, this.props.model)) {
361+
if (this.props.onPaste?.(event, data, this.props.model)) {
362362
// to prevent double handling, allow props.onPaste to skip internal onPaste
363363
return true;
364364
}
365365

366366
const { model } = this.props;
367367
const { partCreator } = model;
368-
const plainText = event.clipboardData.getData("text/plain");
369-
const partsText = event.clipboardData.getData("application/x-element-composer");
368+
const plainText = data.getData("text/plain");
369+
const partsText = data.getData("application/x-element-composer");
370370

371371
let parts: Part[];
372372
if (partsText) {
@@ -387,6 +387,21 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
387387
}
388388
};
389389

390+
private onPaste = (event: ClipboardEvent<HTMLDivElement>): boolean | undefined => {
391+
return this.onPasteHandler(event, event.clipboardData);
392+
};
393+
394+
private onBeforeInput = (event: InputEvent): void => {
395+
// ignore any input while doing IME compositions
396+
if (this.isIMEComposing) {
397+
return;
398+
}
399+
400+
if (event.inputType === "insertFromPaste" && event.dataTransfer) {
401+
this.onPasteHandler(event, event.dataTransfer);
402+
}
403+
};
404+
390405
private onInput = (event: Partial<InputEvent>): void => {
391406
if (!this.editorRef.current) return;
392407
// ignore any input while doing IME compositions
@@ -703,6 +718,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
703718

704719
public componentWillUnmount(): void {
705720
document.removeEventListener("selectionchange", this.onSelectionChange);
721+
this.editorRef.current?.removeEventListener("beforeinput", this.onBeforeInput, true);
706722
this.editorRef.current?.removeEventListener("input", this.onInput, true);
707723
this.editorRef.current?.removeEventListener("compositionstart", this.onCompositionStart, true);
708724
this.editorRef.current?.removeEventListener("compositionend", this.onCompositionEnd, true);
@@ -728,6 +744,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
728744
this.updateEditorState(this.getInitialCaretPosition());
729745
// attach input listener by hand so React doesn't proxy the events,
730746
// as the proxied event doesn't support inputType, which we need.
747+
this.editorRef.current?.addEventListener("beforeinput", this.onBeforeInput, true);
731748
this.editorRef.current?.addEventListener("input", this.onInput, true);
732749
this.editorRef.current?.addEventListener("compositionstart", this.onCompositionStart, true);
733750
this.editorRef.current?.addEventListener("compositionend", this.onCompositionEnd, true);

src/components/views/rooms/SendMessageComposer.tsx

+56-5
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import React, { ClipboardEvent, createRef, KeyboardEvent } from "react";
17+
import React, { createRef, KeyboardEvent, SyntheticEvent } from "react";
1818
import EMOJI_REGEX from "emojibase-regex";
1919
import { IContent, MatrixEvent, IEventRelation, IMentions } from "matrix-js-sdk/src/models/event";
2020
import { DebouncedFunc, throttle } from "lodash";
@@ -61,6 +61,7 @@ import { addReplyToMessageContent } from "../../../utils/Reply";
6161
import { doMaybeLocalRoomAction } from "../../../utils/local-room";
6262
import { Caret } from "../../../editor/caret";
6363
import { IDiff } from "../../../editor/diff";
64+
import { getBlobSafeMimeType } from "../../../utils/blobs";
6465

6566
/**
6667
* Build the mentions information based on the editor model (and any related events):
@@ -667,15 +668,14 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
667668
}
668669
};
669670

670-
private onPaste = (event: ClipboardEvent<HTMLDivElement>): boolean => {
671-
const { clipboardData } = event;
671+
private onPaste = (event: Event | SyntheticEvent, data: DataTransfer): boolean => {
672672
// Prioritize text on the clipboard over files if RTF is present as Office on macOS puts a bitmap
673673
// in the clipboard as well as the content being copied. Modern versions of Office seem to not do this anymore.
674674
// We check text/rtf instead of text/plain as when copy+pasting a file from Finder or Gnome Image Viewer
675675
// it puts the filename in as text/plain which we want to ignore.
676-
if (clipboardData.files.length && !clipboardData.types.includes("text/rtf")) {
676+
if (data.files.length && !data.types.includes("text/rtf")) {
677677
ContentMessages.sharedInstance().sendContentListToRoom(
678-
Array.from(clipboardData.files),
678+
Array.from(data.files),
679679
this.props.room.roomId,
680680
this.props.relation,
681681
this.props.mxClient,
@@ -684,6 +684,57 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
684684
return true; // to skip internal onPaste handler
685685
}
686686

687+
// Safari `Insert from iPhone or iPad`
688+
// data.getData("text/html") returns a string like: <img src="blob:https://...">
689+
if (data.types.includes("text/html")) {
690+
const imgElementStr = data.getData("text/html");
691+
const parser = new DOMParser();
692+
const imgDoc = parser.parseFromString(imgElementStr, "text/html");
693+
694+
if (
695+
imgDoc.getElementsByTagName("img").length !== 1 ||
696+
!imgDoc.querySelector("img")?.src.startsWith("blob:") ||
697+
imgDoc.childNodes.length !== 1
698+
) {
699+
console.log("Failed to handle pasted content as Safari inserted content");
700+
701+
// Fallback to internal onPaste handler
702+
return false;
703+
}
704+
const imgSrc = imgDoc!.querySelector("img")!.src;
705+
706+
fetch(imgSrc).then(
707+
(response) => {
708+
response.blob().then(
709+
(imgBlob) => {
710+
const type = imgBlob.type;
711+
const safetype = getBlobSafeMimeType(type);
712+
const ext = type.split("/")[1];
713+
const parts = response.url.split("/");
714+
const filename = parts[parts.length - 1];
715+
const file = new File([imgBlob], filename + "." + ext, { type: safetype });
716+
ContentMessages.sharedInstance().sendContentToRoom(
717+
file,
718+
this.props.room.roomId,
719+
this.props.relation,
720+
this.props.mxClient,
721+
this.context.replyToEvent,
722+
);
723+
},
724+
(error) => {
725+
console.log(error);
726+
},
727+
);
728+
},
729+
(error) => {
730+
console.log(error);
731+
},
732+
);
733+
734+
// Skip internal onPaste handler
735+
return true;
736+
}
737+
687738
return false;
688739
};
689740

0 commit comments

Comments
 (0)