import { Popover } from 'antd'; import React, { FC, useReducer, useRef, useState } from 'react'; import { useRecoilValue } from 'recoil'; import ContentEditable from 'react-contenteditable'; import dynamic from 'next/dynamic'; import classNames from 'classnames'; import WebsocketService from '../../../services/websocket-service'; import { websocketServiceAtom } from '../../stores/ClientConfigStore'; import { MessageType } from '../../../interfaces/socket-events'; import styles from './ChatTextField.module.scss'; // Lazy loaded components const EmojiPicker = dynamic(() => import('./EmojiPicker').then(mod => mod.EmojiPicker), { ssr: false, }); const SendOutlined = dynamic(() => import('@ant-design/icons/SendOutlined'), { ssr: false, }); const SmileOutlined = dynamic(() => import('@ant-design/icons/SmileOutlined'), { ssr: false, }); export type ChatTextFieldProps = { defaultText?: string; enabled: boolean; focusInput: boolean; }; const characterLimit = 300; function getCaretPosition(node) { const selection = window.getSelection(); if (selection.rangeCount === 0) { return 0; } const range = selection.getRangeAt(0); const preCaretRange = range.cloneRange(); const tempElement = document.createElement('div'); preCaretRange.selectNodeContents(node); preCaretRange.setEnd(range.endContainer, range.endOffset); tempElement.appendChild(preCaretRange.cloneContents()); return tempElement.innerHTML.length; } function setCaretPosition(editableDiv, position) { try { const range = document.createRange(); const sel = window.getSelection(); range.selectNode(editableDiv); range.setStart(editableDiv.childNodes[0], position); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); } catch (e) { console.debug(e); } } 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); const websocketService = useRecoilValue(websocketServiceAtom); const text = useRef(defaultText || ''); const [savedCursorLocation, setSavedCursorLocation] = useState(0); // This is a bit of a hack to force the component to re-render when the text changes. // By default when updating a ref the component doesn't re-render. const [, forceUpdate] = useReducer(x => x + 1, 0); const getCharacterCount = () => text.current.length; const sendMessage = () => { if (!websocketService) { console.log('websocketService is not defined'); return; } let message = text.current; // Strip the opening and closing

tags. message = message.replace(/^

|<\/p>$/g, ''); websocketService.send({ type: MessageType.CHAT, body: message }); // Clear the input. text.current = ''; setCharacterCount(0); forceUpdate(); }; 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'); } // Insert into temp `