import { SendOutlined, SmileOutlined } from '@ant-design/icons'; import { Popover } from 'antd'; import React, { FC, useMemo, useState } from 'react'; import { useRecoilValue } from 'recoil'; import { Transforms, createEditor, BaseEditor, Text, Descendant, Editor, Node, Path } from 'slate'; import { Slate, Editable, withReact, ReactEditor, useSelected, useFocused } from 'slate-react'; import dynamic from 'next/dynamic'; 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)); type CustomElement = { type: 'paragraph' | 'span'; children: CustomText[] } | ImageNode; type CustomText = { text: string }; type EmptyText = { text: string; }; type ImageNode = { type: 'image'; alt: string; src: string; name: string; children: EmptyText[]; }; declare module 'slate' { interface CustomTypes { Editor: BaseEditor & ReactEditor; Element: CustomElement; Text: CustomText; } } const Image = p => { const { attributes, element, children } = p; const selected = useSelected(); const focused = useFocused(); return ( {element.alt} {children} ); }; const withImages = editor => { const { isVoid } = editor; // eslint-disable-next-line no-param-reassign editor.isVoid = element => (element.type === 'image' ? true : isVoid(element)); // eslint-disable-next-line no-param-reassign editor.isInline = element => element.type === 'image'; return editor; }; const serialize = node => { if (Text.isText(node)) { const string = node.text; return string; } let children; if (node.children.length === 0) { children = [{ text: '' }]; } else { children = node.children?.map(n => serialize(n)).join(''); } switch (node.type) { case 'paragraph': return `

${children}

`; case 'image': return `${node.alt}`; default: return children; } }; export type ChatTextFieldProps = {}; export const ChatTextField: FC = () => { const [showEmojis, setShowEmojis] = useState(false); const websocketService = useRecoilValue(websocketServiceAtom); const editor = useMemo(() => withReact(withImages(createEditor())), []); const defaultEditorValue: Descendant[] = [ { type: 'paragraph', children: [{ text: '' }], }, ]; const sendMessage = () => { if (!websocketService) { console.log('websocketService is not defined'); return; } const message = serialize(editor); websocketService.send({ type: MessageType.CHAT, body: message }); // Clear the editor. Transforms.delete(editor, { at: { anchor: Editor.start(editor, []), focus: Editor.end(editor, []), }, }); }; const createImageNode = (alt, src, name): ImageNode => ({ type: 'image', alt, src, name, children: [{ text: '' }], }); const insertImage = (url, name) => { if (!url) return; const { selection } = editor; const image = createImageNode(name, url, name); Transforms.insertNodes(editor, image, { select: true }); if (selection) { const [parentNode, parentPath] = Editor.parent(editor, selection.focus?.path); if (editor.isVoid(parentNode) || Node.string(parentNode).length) { // Insert the new image node after the void node or a node with content Transforms.insertNodes(editor, image, { at: Path.next(parentPath), select: true, }); } else { // If the node is empty, replace it instead // Transforms.removeNodes(editor, { at: parentPath }); Transforms.insertNodes(editor, image, { at: parentPath, select: true }); Editor.normalize(editor, { force: true }); } } else { // Insert the new image node at the bottom of the Editor when selection // is falsey Transforms.insertNodes(editor, image, { select: true }); } }; const onEmojiSelect = (e: any) => { ReactEditor.focus(editor); if (e.url) { // Custom emoji const { url } = e; insertImage(url, url); } else { // Native emoji const { emoji } = e; Transforms.insertText(editor, emoji); } }; const onCustomEmojiSelect = (e: any) => { ReactEditor.focus(editor); const { url } = e; insertImage(url, url); }; const onKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); sendMessage(); } }; const renderElement = p => { switch (p.element.type) { case 'image': return ; default: return

; } }; return (

} trigger="click" onVisibleChange={visible => setShowEmojis(visible)} visible={showEmojis} />
); };