commit 789b88d10ff06abf8fb9ff0a315fb486a0ad832b
parent 553c585fc9702737e4a4eebd02630f4de0147ab7
Author: MTRNord <mtrnord1@gmail.com>
Date: Mon, 8 May 2023 02:56:39 +0200
Fix keyboard bindings for enter actions
Diffstat:
3 files changed, 131 insertions(+), 56 deletions(-)
diff --git a/src/components/events/messageEvent.tsx b/src/components/events/messageEvent.tsx
@@ -46,7 +46,7 @@ const MessageEvent: FC<MessageEventProps> = memo(({ event, roomID, hasPreviousEv
const renderCorrectMessage = (event: IRoomEvent) => {
if (isRoomMessageTextEvent(event)) {
if (event.content.format === "org.matrix.custom.html") {
- const sanitized = DOMPurify.sanitize(event.content.formatted_body!, {
+ let sanitized = DOMPurify.sanitize(event.content.formatted_body!, {
ADD_TAGS: [
"font",
"del",
@@ -88,22 +88,22 @@ const MessageEvent: FC<MessageEventProps> = memo(({ event, roomID, hasPreviousEv
"summary"
]
})
- let linkified = linkifyHtml(sanitized, linkifyOptions);
// Extract code and language from the html
const codeRegex = /<pre><code (?:class="language-(?<language>.*?)")?.*?>(?<code>[\s\S]*?)<\/code><\/pre>/;
- const code = codeRegex.exec(linkified);
+ const code = codeRegex.exec(sanitized);
if (code?.groups?.["code"]) {
if (code.groups?.["language"]) {
// Highlight the code
const highlighted = hljs.highlight(code.groups?.["code"], { language: code.groups?.["language"] }).value;
- linkified = linkified.replace(code.groups?.["code"], `${highlighted}`);
+ sanitized = sanitized.replace(code.groups?.["code"], `${highlighted}`);
} else {
// Highlight the code
const highlighted = hljs.highlightAuto(code.groups?.["code"]).value;
- linkified = linkified.replace(code[0], `${highlighted}`);
+ sanitized = sanitized.replace(code[0], `${highlighted}`);
}
}
+ const linkified = linkifyHtml(sanitized, linkifyOptions);
// TODO: sanitize the attributes allowed by matrix spec
return (
diff --git a/src/components/input/chat/customNodes/CustomParagraphNode.ts b/src/components/input/chat/customNodes/CustomParagraphNode.ts
@@ -1,4 +1,4 @@
-import { $applyNodeReplacement, $getRoot, DOMExportOutput, LexicalEditor, ParagraphNode, SerializedElementNode } from "lexical";
+import { $applyNodeReplacement, $getRoot, DOMExportOutput, LexicalEditor, ParagraphNode, SerializedElementNode, SerializedParagraphNode } from "lexical";
export class CustomParagraphNode extends ParagraphNode {
static getType(): string {
@@ -38,6 +38,10 @@ export class CustomParagraphNode extends ParagraphNode {
node.type = "custom-paragraph";
return node;
}
+
+ static importJSON(node: SerializedParagraphNode): CustomParagraphNode {
+ return super.importJSON(node);
+ }
}
export function $createCustomParagraphNode(): CustomParagraphNode {
diff --git a/src/components/input/chat/input.tsx b/src/components/input/chat/input.tsx
@@ -28,9 +28,51 @@ import { FC, memo, useEffect, useState } from 'react';
import { Send } from 'lucide-react';
import { useRoom } from '../../../app/sdk/client';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
-import { CLEAR_EDITOR_COMMAND, CLEAR_HISTORY_COMMAND, ParagraphNode } from 'lexical';
+import { $getSelection, $isRangeSelection, CLEAR_EDITOR_COMMAND, CLEAR_HISTORY_COMMAND, COMMAND_PRIORITY_CRITICAL, INSERT_PARAGRAPH_COMMAND, KEY_ENTER_COMMAND, ParagraphNode } from 'lexical';
import { useLocation } from 'react-router-dom';
+export const CAN_USE_DOM: boolean =
+ typeof window !== 'undefined' &&
+ typeof window.document !== 'undefined' &&
+ typeof window.document.createElement !== 'undefined';
+
+
+declare global {
+ interface Document {
+ documentMode?: unknown;
+ }
+
+ interface Window {
+ MSStream?: unknown;
+ }
+}
+
+const documentMode =
+ CAN_USE_DOM && 'documentMode' in document ? document.documentMode : null;
+
+export const CAN_USE_BEFORE_INPUT: boolean =
+ CAN_USE_DOM && 'InputEvent' in window && !documentMode
+ ? 'getTargetRanges' in new window.InputEvent('input')
+ : false;
+
+export const IS_SAFARI: boolean =
+ CAN_USE_DOM && /Version\/[\d.]+.*Safari/.test(navigator.userAgent);
+
+export const IS_IOS: boolean =
+ CAN_USE_DOM &&
+ /iPad|iPhone|iPod/.test(navigator.userAgent) &&
+ !window.MSStream;
+
+
+// Keep these in case we need to use them in the future.
+// export const IS_WINDOWS: boolean = CAN_USE_DOM && /Win/.test(navigator.platform);
+export const IS_CHROME: boolean =
+ CAN_USE_DOM && /^(?=.*Chrome).*/i.test(navigator.userAgent);
+// export const canUseTextInputEvent: boolean = CAN_USE_DOM && 'TextEvent' in window && !documentMode;
+
+export const IS_APPLE_WEBKIT =
+ CAN_USE_DOM && /AppleWebKit\/[\d.]+/.test(navigator.userAgent) && !IS_CHROME;
+
type ChatInputProps = {
/**
* The Namespace
@@ -51,14 +93,6 @@ type SendButtonProps = {
* The current Room
*/
roomID?: string
- /**
- * The HTML message
- */
- htmlMessage: string
- /**
- * The plain text message
- */
- plainMessage: string
/**
* Callback when sending starts
*/
@@ -69,16 +103,49 @@ type SendButtonProps = {
onStopSending: () => void
};
-const SendButton: FC<SendButtonProps> = memo(({ roomID, htmlMessage, plainMessage, onStartSending, onStopSending }: SendButtonProps) => {
+const SendButton: FC<SendButtonProps> = memo(({ roomID, onStartSending, onStopSending }: SendButtonProps) => {
const room = useRoom(roomID || "");
const [editor] = useLexicalComposerContext();
- return <Send size={45} stroke='unset' className='stroke-slate-600 rounded m-4 hover:bg-slate-300 hover:stroke-slate-500 p-2 cursor-pointer' onClick={async () => {
+ const sendMessage = async () => {
// TODO: Sanitize the html and send message to room
if (!room) {
return;
}
+ let htmlMessage = $generateHtmlFromNodes(editor);
+ // TODO: Make sure that we strip any non matrix stuff
+ const codeRegex = /(?<all><code .* (?:data-highlight-language="(?<language>.*?)")(?: .*?)?>(?<code>[\s\S]*?)<\/code>)/;
+ let matched = codeRegex.exec(htmlMessage);
+ while (matched !== null) {
+ if (matched) {
+ const { groups } = matched;
+ if (groups) {
+ let { all, language, code } = groups;
+
+ const spanRegex = /<span(?: class=".*?")?>(?<content>.*?)<\/span>/;
+ let matchedspans = spanRegex.exec(code);
+ while (matchedspans !== null) {
+ if (matchedspans) {
+ const { groups } = matchedspans;
+ if (groups) {
+ const { content } = groups;
+
+ code = code.replaceAll(matchedspans[0], content)
+ }
+ }
+ matchedspans = spanRegex.exec(code)
+ }
+
+ code = code.replaceAll("<br>", "\n")
+ htmlMessage = htmlMessage.replace(all, `<pre><code class="language-${language}">${code}</code></pre>`)
+ }
+ }
+ matched = codeRegex.exec(htmlMessage)
+ }
+ const plainMessage = $convertToMarkdownString(TRANSFORMERS);
+
+ console.log(htmlMessage)
// TODO: local echo
if ((htmlMessage === "" && plainMessage === "") || htmlMessage === '<p class="editor-paragraph"><br></p>') {
return;
@@ -106,7 +173,47 @@ const SendButton: FC<SendButtonProps> = memo(({ roomID, htmlMessage, plainMessag
}
}
onStopSending();
- }} />
+ }
+
+ useEffect(() => {
+ editor.registerCommand<KeyboardEvent | null>(
+ KEY_ENTER_COMMAND,
+ (event) => {
+ const selection = $getSelection();
+ if (!$isRangeSelection(selection)) {
+ return false;
+ }
+ if (event !== null) {
+ // If we have beforeinput, then we can avoid blocking
+ // the default behavior. This ensures that the iOS can
+ // intercept that we're actually inserting a paragraph,
+ // and autocomplete, autocapitalize etc work as intended.
+ // This can also cause a strange performance issue in
+ // Safari, where there is a noticeable pause due to
+ // preventing the key down of enter.
+ if (
+ (IS_IOS || IS_SAFARI || IS_APPLE_WEBKIT) &&
+ CAN_USE_BEFORE_INPUT
+ ) {
+ return false;
+ }
+ event.preventDefault();
+ if (event.shiftKey) {
+ return editor.dispatchCommand(INSERT_PARAGRAPH_COMMAND, undefined);
+ }
+ }
+ sendMessage();
+ return editor.dispatchCommand(INSERT_PARAGRAPH_COMMAND, undefined);
+ },
+ COMMAND_PRIORITY_CRITICAL,
+ )
+ }, [editor])
+
+ return <Send
+ size={45}
+ stroke='unset'
+ className='stroke-slate-600 rounded m-4 hover:bg-slate-300 hover:stroke-slate-500 p-2 cursor-pointer'
+ onClick={sendMessage} />
});
@@ -146,8 +253,6 @@ const RoomChangePlugin: FC<RoomChangeProps> = ({ roomID }) => {
}
const ChatInput: FC<ChatInputProps> = memo(({ namespace, roomID }: ChatInputProps) => {
- const [htmlMessage, setHtmlMessage] = useState<string>("");
- const [plainMessage, setPlainMessage] = useState<string>("");
const [sending, setSending] = useState(false);
const initialConfig: InitialConfigType = {
@@ -191,41 +296,7 @@ const ChatInput: FC<ChatInputProps> = memo(({ namespace, roomID }: ChatInputProp
// Convert editor state to both html and markdown.
// If there is no formatting then just use the plain text.
editorState.read(() => {
- let html = $generateHtmlFromNodes(editor);
- // TODO: Make sure that we strip any non matrix stuff
- const codeRegex = /(?<all><code .* (?:data-highlight-language="(?<language>.*?)")(?: .*?)?>(?<code>[\s\S]*?)<\/code>)/;
- let matched = codeRegex.exec(html);
- while (matched !== null) {
- if (matched) {
- const { groups } = matched;
- if (groups) {
- let { all, language, code } = groups;
-
- const spanRegex = /<span(?: class=".*?")?>(?<content>.*?)<\/span>/;
- let matchedspans = spanRegex.exec(code);
- while (matchedspans !== null) {
- if (matchedspans) {
- const { groups } = matchedspans;
- if (groups) {
- const { content } = groups;
-
- code = code.replaceAll(matchedspans[0], content)
- }
- }
- matchedspans = spanRegex.exec(code)
- }
-
- code = code.replaceAll("<br>", "\n")
- html = html.replace(all, `<pre><code class="language-${language}">${code}</code></pre>`)
-
-
- }
- }
- matched = codeRegex.exec(html)
- }
- setHtmlMessage(html);
- const markdown = $convertToMarkdownString(TRANSFORMERS);
- setPlainMessage(markdown);
+
});
// TODO: we need some send button
}} />
@@ -239,7 +310,7 @@ const ChatInput: FC<ChatInputProps> = memo(({ namespace, roomID }: ChatInputProp
{/*<TreeViewPlugin />*/}
</div>
</div>
- <SendButton roomID={roomID} htmlMessage={htmlMessage} plainMessage={plainMessage} onStartSending={() => { console.log("Sending"); setSending(true) }} onStopSending={() => { setSending(false) }} />
+ <SendButton roomID={roomID} onStartSending={() => { console.log("Sending"); setSending(true) }} onStopSending={() => { setSending(false) }} />
</LexicalComposer>
</div>