From 7e793ec0ba6835f40ef038a6d8c1b643f27db71e Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 28 Oct 2024 00:28:59 +0200 Subject: [PATCH] web/composer: surround selection with markdown when pasting link --- web/src/ui/composer/Autocompleter.tsx | 7 ++----- web/src/ui/composer/MessageComposer.tsx | 27 ++++++++++++++++++++----- web/src/util/markdown.ts | 21 +++++++++++++++++++ 3 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 web/src/util/markdown.ts diff --git a/web/src/ui/composer/Autocompleter.tsx b/web/src/ui/composer/Autocompleter.tsx index 7bc31ee..fdd49cc 100644 --- a/web/src/ui/composer/Autocompleter.tsx +++ b/web/src/ui/composer/Autocompleter.tsx @@ -17,6 +17,7 @@ import { JSX, use, useEffect } from "react" import { getAvatarURL, getMediaURL } from "@/api/media.ts" import { RoomStateStore, useCustomEmojis } from "@/api/statestore" import { Emoji, emojiToMarkdown, useSortedAndFilteredEmojis } from "@/util/emoji" +import { escapeMarkdown } from "@/util/markdown.ts" import useEvent from "@/util/useEvent.ts" import ClientContext from "../ClientContext.ts" import type { ComposerState } from "./MessageComposer.tsx" @@ -111,11 +112,7 @@ export const EmojiAutocompleter = ({ params, room, ...rest }: AutocompleterProps return useAutocompleter({ params, room, ...rest, items, ...emojiFuncs }) } -const escapeDisplayname = (input: string) => input - .replace("\n", " ") - .replace(/([\\`*_[\]])/g, "\\$1") - .replace("<", "<") - .replace(">", ">") +const escapeDisplayname = (input: string) => escapeMarkdown(input).replace("\n", " ") const userFuncs = { getText: (user: AutocompleteUser) => diff --git a/web/src/ui/composer/MessageComposer.tsx b/web/src/ui/composer/MessageComposer.tsx index e3f28d8..ec37310 100644 --- a/web/src/ui/composer/MessageComposer.tsx +++ b/web/src/ui/composer/MessageComposer.tsx @@ -26,6 +26,7 @@ import type { RoomID, } from "@/api/types" import { PartialEmoji, emojiToMarkdown } from "@/util/emoji" +import { escapeMarkdown } from "@/util/markdown.ts" import useEvent from "@/util/useEvent.ts" import ClientContext from "../ClientContext.ts" import EmojiPicker from "../emojipicker/EmojiPicker.tsx" @@ -266,11 +267,26 @@ const MessageComposer = () => { const onAttachFile = useEvent( (evt: React.ChangeEvent) => doUploadFile(evt.target.files?.[0]), ) - useEffect(() => { - const listener = (evt: ClipboardEvent) => doUploadFile(evt.clipboardData?.files?.[0]) - document.addEventListener("paste", listener) - return () => document.removeEventListener("paste", listener) - }, [doUploadFile]) + const onPaste = useEvent((evt: React.ClipboardEvent) => { + const file = evt.clipboardData?.files?.[0] + const text = evt.clipboardData.getData("text/plain") + const input = evt.currentTarget + if (file) { + doUploadFile(file) + } else if ( + input.selectionStart !== input.selectionEnd + && (text.startsWith("http://") || text.startsWith("https://") || text.startsWith("matrix:")) + ) { + setState({ + text: `${state.text.slice(0, input.selectionStart)}[${ + escapeMarkdown(state.text.slice(input.selectionStart, input.selectionEnd)) + }](${escapeMarkdown(text)})${state.text.slice(input.selectionEnd)}`, + }) + } else { + return + } + evt.preventDefault() + }) // To ensure the cursor jumps to the end, do this in an effect rather than as the initial value of useState // To try to avoid the input bar flashing, use useLayoutEffect instead of useEffect useLayoutEffect(() => { @@ -370,6 +386,7 @@ const MessageComposer = () => { onKeyDown={onComposerKeyDown} onKeyUp={onComposerCaretChange} onClick={onComposerCaretChange} + onPaste={onPaste} onChange={onChange} placeholder="Send a message" id="message-composer" diff --git a/web/src/util/markdown.ts b/web/src/util/markdown.ts new file mode 100644 index 0000000..e2db3bb --- /dev/null +++ b/web/src/util/markdown.ts @@ -0,0 +1,21 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +export const escapeMarkdown = (input: string) => input + .replace(/([\\`*_[\]])/g, "\\$1") + .replace("<", "<") + .replace(">", ">")