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

Support Insert from iPhone or iPad in Safari #10851

Merged
merged 24 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
87e816f
Handle `insertFromPaste` properly for wider ranging paste support
t3chguy May 10, 2023
0e60b97
Support `Insert from iPhone or iPad` in Safari
SuperKenVery May 10, 2023
20f5339
Support Safari `Insert from iPhone or iPad`
SuperKenVery May 11, 2023
745ed2a
Handle `insertFromPaste` properly for wider ranging paste support
t3chguy May 10, 2023
caa13fd
Support `Insert from iPhone or iPad` in Safari
SuperKenVery May 10, 2023
7fdf900
Support Safari `Insert from iPhone or iPad`
SuperKenVery May 11, 2023
7dabe87
Merge branch 't3chguy/onbeforeinput' of github.com:SuperKenVery/matri…
SuperKenVery May 11, 2023
b30a357
Use the MIMEType filter
SuperKenVery May 11, 2023
a07f4ff
Respect reply; don't leak source
SuperKenVery May 17, 2023
88cc152
Merge branch 'develop' into t3chguy/onbeforeinput
t3chguy May 17, 2023
5444b99
Check null; Run prettier
SuperKenVery May 17, 2023
ebc53ed
Add more check to ensure it's inserted from Safari
SuperKenVery May 17, 2023
07a3c7a
Pass strict null checks
SuperKenVery May 17, 2023
ad99392
tsconfig.json: Enable strict null check
SuperKenVery May 17, 2023
5188490
Revert tsconfig change
SuperKenVery May 21, 2023
4a82341
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into…
SuperKenVery May 22, 2023
1bd0db0
Merge branch 't3chguy/onbeforeinput' of github.com:SuperKenVery/matri…
SuperKenVery May 22, 2023
e53505f
Update src/components/views/rooms/SendMessageComposer.tsx: Simplify c…
SuperKenVery May 23, 2023
511d2a1
Disallow nodes other than an image
SuperKenVery May 23, 2023
f4c7a0b
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into…
SuperKenVery May 23, 2023
e1755b8
Use blob url as filename
SuperKenVery May 24, 2023
fccd197
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into…
SuperKenVery May 24, 2023
48ff504
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into…
SuperKenVery May 25, 2023
0c2e985
Merge branch 't3chguy/onbeforeinput' of github.com:SuperKenVery/matri…
SuperKenVery May 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions src/components/views/rooms/BasicMessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ limitations under the License.
*/

import classNames from "classnames";
import React, { createRef, ClipboardEvent } from "react";
import React, { createRef, ClipboardEvent, SyntheticEvent } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import EMOTICON_REGEX from "emojibase-regex/emoticon";
Expand Down Expand Up @@ -108,7 +108,7 @@ interface IProps {
disabled?: boolean;

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

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

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

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

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

private onPaste = (event: ClipboardEvent<HTMLDivElement>): boolean | undefined => {
return this.onPasteHandler(event, event.clipboardData);
};

private onBeforeInput = (event: InputEvent): void => {
// ignore any input while doing IME compositions
if (this.isIMEComposing) {
return;
}

if (event.inputType === "insertFromPaste" && event.dataTransfer) {
this.onPasteHandler(event, event.dataTransfer);
}
};

private onInput = (event: Partial<InputEvent>): void => {
if (!this.editorRef.current) return;
// ignore any input while doing IME compositions
Expand Down Expand Up @@ -703,6 +718,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>

public componentWillUnmount(): void {
document.removeEventListener("selectionchange", this.onSelectionChange);
this.editorRef.current?.removeEventListener("beforeinput", this.onBeforeInput, true);
this.editorRef.current?.removeEventListener("input", this.onInput, true);
this.editorRef.current?.removeEventListener("compositionstart", this.onCompositionStart, true);
this.editorRef.current?.removeEventListener("compositionend", this.onCompositionEnd, true);
Expand All @@ -728,6 +744,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
this.updateEditorState(this.getInitialCaretPosition());
// attach input listener by hand so React doesn't proxy the events,
// as the proxied event doesn't support inputType, which we need.
this.editorRef.current?.addEventListener("beforeinput", this.onBeforeInput, true);
this.editorRef.current?.addEventListener("input", this.onInput, true);
this.editorRef.current?.addEventListener("compositionstart", this.onCompositionStart, true);
this.editorRef.current?.addEventListener("compositionend", this.onCompositionEnd, true);
Expand Down
60 changes: 55 additions & 5 deletions src/components/views/rooms/SendMessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

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

/**
* Build the mentions information based on the editor model (and any related events):
Expand Down Expand Up @@ -666,15 +667,14 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
}
};

private onPaste = (event: ClipboardEvent<HTMLDivElement>): boolean => {
const { clipboardData } = event;
private onPaste = (event: Event | SyntheticEvent, data: DataTransfer): boolean => {
// 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 (clipboardData.files.length && !clipboardData.types.includes("text/rtf")) {
if (data.files.length && !data.types.includes("text/rtf")) {
ContentMessages.sharedInstance().sendContentListToRoom(
Array.from(clipboardData.files),
Array.from(data.files),
this.props.room.roomId,
this.props.relation,
this.props.mxClient,
Expand All @@ -683,6 +683,56 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
return true; // to skip internal onPaste handler
}

// 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");

if (
imgDoc.querySelector("img") == null ||
imgDoc.querySelector("img")!.src == null ||
imgDoc.getElementsByTagName("img").length != 1 ||
!imgDoc.querySelector("img")!.src.startsWith("blob:")
SuperKenVery marked this conversation as resolved.
Show resolved Hide resolved
) {
console.log("Failed to handle pasted content as Safari inserted content");

// Fallback to internal onPaste handler
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 file = new File([imgBlob], "file." + ext, { type: safetype });
SuperKenVery marked this conversation as resolved.
Show resolved Hide resolved
ContentMessages.sharedInstance().sendContentToRoom(
file,
this.props.room.roomId,
this.props.relation,
this.props.mxClient,
this.context.replyToEvent,
);
},
(error) => {
console.log(error);
},
);
},
(error) => {
console.log(error);
},
);

// Skip internal onPaste handler
return true;
}

return false;
};

Expand Down