From a354787a9e04c13eb6a48f770f2e439d5b559e51 Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Tue, 27 Jun 2023 14:45:45 -0700 Subject: [PATCH] feat(chat): support line breaks and pasted content. Closes #3108 --- core/chat/events/events.go | 2 +- core/chat/messageRendering_test.go | 12 +- web/.stylelintrc.json | 3 +- .../chat/ChatTextField/ChatTextField.tsx | 136 ++++++-------- .../ChatUserMessage.module.scss | 6 + web/package-lock.json | 170 +++++++++++++++++- web/package.json | 2 + 7 files changed, 239 insertions(+), 92 deletions(-) diff --git a/core/chat/events/events.go b/core/chat/events/events.go index 56a642e70..d50d9a259 100644 --- a/core/chat/events/events.go +++ b/core/chat/events/events.go @@ -149,7 +149,7 @@ func sanitize(raw string) string { // Allow breaks p.AllowElements("br") - p.AllowElementsContent("p") + p.AllowElements("p") // Allow img tags from the the local emoji directory only p.AllowAttrs("src").Matching(_sanitizeReSrcMatch).OnElements("img") diff --git a/core/chat/messageRendering_test.go b/core/chat/messageRendering_test.go index 0a2895394..382d83cdb 100644 --- a/core/chat/messageRendering_test.go +++ b/core/chat/messageRendering_test.go @@ -16,11 +16,11 @@ Here is an iframe [test link](http://owncast.online) ` - expected := `Test one two three! I go to http://yahoo.com and search for sports and answers. -Here is an iframe + expected := `

Test one two three! I go to http://yahoo.com and search for sports and answers. +Here is an iframe

blah blah blah -test link -` +

test link +

` result := events.RenderAndSanitize(messageContent) if result != expected { @@ -31,7 +31,7 @@ blah blah blah // Test to make sure we block remote images in chat messages. func TestBlockRemoteImages(t *testing.T) { messageContent := ` test ![](https://via.placeholder.com/img/emoji/350x150)` - expected := `test` + expected := `

test

` result := events.RenderAndSanitize(messageContent) if result != expected { @@ -42,7 +42,7 @@ func TestBlockRemoteImages(t *testing.T) { // Test to make sure emoji images are allowed in chat messages. func TestAllowEmojiImages(t *testing.T) { messageContent := `:beerparrot: test ![](/img/emoji/beerparrot.gif)` - expected := `:beerparrot: test ` + expected := `

:beerparrot: test

` result := events.RenderAndSanitize(messageContent) if result != expected { diff --git a/web/.stylelintrc.json b/web/.stylelintrc.json index 9610e88b0..8d53a4ce1 100644 --- a/web/.stylelintrc.json +++ b/web/.stylelintrc.json @@ -1,6 +1,7 @@ { "extends": "stylelint-config-standard-scss", "rules": { - "selector-class-pattern": null + "selector-class-pattern": null, + "no-descending-specificity": null } } \ No newline at end of file diff --git a/web/components/chat/ChatTextField/ChatTextField.tsx b/web/components/chat/ChatTextField/ChatTextField.tsx index 865bcaba2..cc38b905c 100644 --- a/web/components/chat/ChatTextField/ChatTextField.tsx +++ b/web/components/chat/ChatTextField/ChatTextField.tsx @@ -1,7 +1,9 @@ import { Popover } from 'antd'; -import React, { FC, useReducer, useRef, useState } from 'react'; +import React, { FC, useEffect, useReducer, useRef, useState } from 'react'; import { useRecoilValue } from 'recoil'; import ContentEditable from 'react-contenteditable'; +import sanitizeHtml from 'sanitize-html'; + import dynamic from 'next/dynamic'; import classNames from 'classnames'; import WebsocketService from '../../../services/websocket-service'; @@ -64,43 +66,6 @@ function setCaretPosition(editableDiv, position) { } } -function convertToText(str = '') { - // Ensure string. - let value = String(str); - - // Convert encoding. - value = value.replace(/ /gi, ' '); - value = value.replace(/&/gi, '&'); - - // Replace `
`. - value = value.replace(/
/gi, '\n'); - - // Replace `
` (from Chrome). - value = value.replace(/
/gi, '\n'); - - // Replace `

` (from IE). - value = value.replace(/

/gi, '\n'); - - // Cleanup the emoji titles. - value = value.replace(/\u200C{2}/gi, ''); - - // Trim each line. - value = value - .split('\n') - .map((line = '') => line.trim()) - .join('\n'); - - // No more than 2x newline, per "paragraph". - value = value.replace(/\n\n+/g, '\n\n'); - - // Clean up spaces. - value = value.replace(/[ ]+/g, ' '); - value = value.trim(); - - // Expose string. - return value; -} - export const ChatTextField: FC = ({ defaultText, enabled, focusInput }) => { const [showEmojis, setShowEmojis] = useState(false); const [characterCount, setCharacterCount] = useState(defaultText?.length); @@ -132,44 +97,21 @@ export const ChatTextField: FC = ({ defaultText, enabled, fo }; const insertTextAtCursor = (textToInsert: string) => { - const output = [ - text.current.slice(0, savedCursorLocation), - textToInsert, - text.current.slice(savedCursorLocation), - ].join(''); - text.current = output; - forceUpdate(); - }; - - const convertOnPaste = (event: React.ClipboardEvent) => { - // Prevent paste. - event.preventDefault(); - - // Set later. - let value = ''; - - // Does method exist? - const hasEventClipboard = !!( - event.clipboardData && - typeof event.clipboardData === 'object' && - typeof event.clipboardData.getData === 'function' - ); - - // Get clipboard data? - if (hasEventClipboard) { - value = event.clipboardData.getData('text/plain'); + let cursorLocation; + if (savedCursorLocation > 0) { + cursorLocation = savedCursorLocation; + } else { + cursorLocation = getCaretPosition(document.getElementById('chat-input')); } - // Insert into temp `