diff --git a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts index a926f8540d27..96c05b765011 100644 --- a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts +++ b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts @@ -4,10 +4,11 @@ import { Meteor } from 'meteor/meteor'; import type { ComposerAPI } from '../../../../client/lib/chats/ChatAPI'; import { withDebouncing } from '../../../../lib/utils/highOrderFunctions'; +import buildMarkdown from '../../../ui-utils/client/lib/buildMarkDown'; import type { FormattingButton } from './messageBoxFormatting'; import { formattingButtons } from './messageBoxFormatting'; -export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string): ComposerAPI => { +export const createComposerAPI = (input: any, storageID: string): ComposerAPI => { const triggerEvent = (input: HTMLTextAreaElement, evt: string): void => { const event = new Event(evt, { bubbles: true }); // TODO: Remove this hack for react to trigger onChange @@ -83,6 +84,32 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) !skipFocus && focus(); }; + const setTextV2 = (text: string) => { + const selection = window.getSelection(); + + if (selection && selection.rangeCount > 0) { + const range = selection?.getRangeAt(0); + console.log(range); + + const textNode = document.createTextNode(text); + console.log(textNode); + + range?.deleteContents(); + range?.insertNode(textNode); + + range?.setStartAfter(textNode); + range?.setEndAfter(textNode); + + selection?.removeAllRanges(); + + selection?.addRange(range); + console.log('here'); + } else { + input.innerHTML += text; + console.log('here2'); + } + }; + const insertText = (text: string): void => { setText(text, { selection: ({ start, end }) => ({ @@ -92,7 +119,14 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) }); }; + const insertTextV2 = (text: string): void => { + setTextV2(text); + }; + const clear = (): void => { + while (input.childNodes[0].firstChild) { + input.childNodes[0].removeChild(input.childNodes[0].firstChild); + } setText(''); }; @@ -260,6 +294,10 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) focus(); }; + const wrapSelectionV2 = (): void => { + setText(buildMarkdown(input)); + }; + const insertNewLine = (): void => insertText('\n'); setText(Meteor._localStorage.getItem(storageID) ?? '', { @@ -314,6 +352,7 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) }, release, wrapSelection, + wrapSelectionV2, get text(): string { return input.value; }, @@ -332,6 +371,8 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) setRecordingVideo, insertText, setText, + setTextV2, + insertTextV2, clear, focus, replyWith, diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts b/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts index 8c170f894aa4..92597814e835 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts +++ b/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts @@ -8,6 +8,7 @@ export type FormattingButton = label: TranslationKey; icon: IconName; pattern: string; + buttonName?: string; // text?: () => string | undefined; command?: string; link?: string; @@ -24,28 +25,33 @@ export const formattingButtons: ReadonlyArray = [ { label: 'Bold', icon: 'bold', + buttonName: 'ql-bold', pattern: '*{{text}}*', command: 'b', }, { label: 'Italic', icon: 'italic', + buttonName: 'ql-italic', pattern: '_{{text}}_', command: 'i', }, { label: 'Strike', icon: 'strike', + buttonName: 'ql-strike', pattern: '~{{text}}~', }, { label: 'Inline_code', icon: 'code', + buttonName: 'ql-code', pattern: '`{{text}}`', }, { label: 'Multi_line', icon: 'multiline', + buttonName: 'ql-code-block', pattern: '```\n{{text}}\n``` ', }, { diff --git a/apps/meteor/app/ui-utils/client/lib/buildMarkDown.ts b/apps/meteor/app/ui-utils/client/lib/buildMarkDown.ts new file mode 100644 index 000000000000..572b1262135a --- /dev/null +++ b/apps/meteor/app/ui-utils/client/lib/buildMarkDown.ts @@ -0,0 +1,62 @@ +const elementTagMaps = { + strong: '*', + em: '_', + s: '~', + li: '- ', + pre: '```', + code: '`', + p: '\n', + br: '', + span: '', +}; + +const makeListMarkDown = (element: HTMLElement) => { + let text = ''; + for (let i = 0; i < element.childNodes.length - 1; i++) { + const child = element.childNodes[i]; + if (element.nodeName.toLowerCase() === 'ol') { + text += `${i + 1}. `; + } else { + const mdSymbol = elementTagMaps[child.nodeName.toLowerCase() as keyof typeof elementTagMaps] || ''; + text += mdSymbol; + } + text += parseMarkdown(child as HTMLElement); + text += '\n'; + } + return text; +}; + +const makeCodeBlockMarkDown = (element: HTMLElement) => { + let text = `${elementTagMaps.pre}\n`; + text += parseMarkdown(element as HTMLElement); + text += elementTagMaps.pre; + return text; +}; + +const parseMarkdown = (element: HTMLElement) => { + let text = ''; + for (const child of element.childNodes) { + if (child.nodeType === Node.TEXT_NODE) { + text += child.textContent || ''; + } else if (child.nodeType === Node.ELEMENT_NODE) { + if (child.nodeName.toLowerCase() === 'ul' || child.nodeName.toLowerCase() === 'ol') { + text += makeListMarkDown(child as HTMLElement); + continue; + } else if (child.nodeName.toLowerCase() === 'pre') { + text += makeCodeBlockMarkDown(child as HTMLElement); + continue; + } + const mdSymbol = elementTagMaps[child.nodeName.toLowerCase() as keyof typeof elementTagMaps] || ''; + text += mdSymbol; + text += parseMarkdown(child as HTMLElement); + if (child.nodeName.toLowerCase() !== 'p') text += mdSymbol; + } + } + return text; +}; + +const buildMarkdown = (element: HTMLElement) => { + return parseMarkdown(element.childNodes[0] as HTMLElement); +}; + +export default buildMarkdown; diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 36f40f404a4f..38ad5b75d0bc 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -19,8 +19,11 @@ export type ComposerAPI = { | ((previous: { readonly start: number; readonly end: number }) => { readonly start?: number; readonly end?: number }); }, ): void; + setTextV2(text: string): void; wrapSelection(pattern: string): void; + wrapSelectionV2(pattern?: string): void; insertText(text: string): void; + insertTextV2(text: string): void; insertNewLine(): void; clear(): void; focus(): void; diff --git a/apps/meteor/client/providers/EmojiPickerProvider.tsx b/apps/meteor/client/providers/EmojiPickerProvider.tsx index f7d14b317d54..fc68611e2768 100644 --- a/apps/meteor/client/providers/EmojiPickerProvider.tsx +++ b/apps/meteor/client/providers/EmojiPickerProvider.tsx @@ -93,8 +93,10 @@ const EmojiPickerProvider = ({ children }: { children: ReactNode }): ReactElemen [recentEmojis, setRecentEmojis, updateEmojiListByCategory, addFrequentEmojis], ); - const open = useCallback((ref: Element, callback: (emoji: string) => void) => { - return setEmojiPicker( setEmojiPicker(null)} onPickEmoji={(emoji) => callback(emoji)} />); + const open = useCallback((ref: Element, callback: (emoji: string, image?: string) => void) => { + return setEmojiPicker( + setEmojiPicker(null)} onPickEmoji={(emoji, image) => callback(emoji, image)} />, + ); }, []); const handlePreview = useCallback((emoji: string, name: string) => setEmojiToPreview({ emoji, name }), [setEmojiToPreview]); diff --git a/apps/meteor/client/views/composer/EmojiPicker/EmojiPicker.tsx b/apps/meteor/client/views/composer/EmojiPicker/EmojiPicker.tsx index 971005e3fa8e..8b4572293b11 100644 --- a/apps/meteor/client/views/composer/EmojiPicker/EmojiPicker.tsx +++ b/apps/meteor/client/views/composer/EmojiPicker/EmojiPicker.tsx @@ -28,7 +28,7 @@ import ToneSelectorWrapper from './ToneSelector/ToneSelectorWrapper'; type EmojiPickerProps = { reference: Element; onClose: () => void; - onPickEmoji: (emoji: string) => void; + onPickEmoji: (emoji: string, image?: string) => void; }; const EmojiPicker = ({ reference, onClose, onPickEmoji }: EmojiPickerProps) => { @@ -104,6 +104,7 @@ const EmojiPicker = ({ reference, onClose, onPickEmoji }: EmojiPickerProps) => { event.stopPropagation(); const _emoji = event.currentTarget.dataset?.emoji; + const _image = event.currentTarget.textContent; if (!_emoji) { return; @@ -121,7 +122,7 @@ const EmojiPicker = ({ reference, onClose, onPickEmoji }: EmojiPickerProps) => { setSearchTerm(''); - onPickEmoji(_emoji + tone); + onPickEmoji(_emoji + tone, _image || undefined); addRecentEmoji(_emoji + tone); onClose(); }; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index f7d5bfaf58e3..bacc8bd91b0a 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -1,3 +1,4 @@ +/* eslint-disable complexity */ import type { IMessage, ISubscription } from '@rocket.chat/core-typings'; import { Button, Tag, Box } from '@rocket.chat/fuselage'; import { useContentBoxSize, useMutableCallback } from '@rocket.chat/fuselage-hooks'; @@ -10,10 +11,10 @@ import { MessageComposerActionsDivider, MessageComposerToolbarSubmit, } from '@rocket.chat/ui-composer'; -import { useTranslation, useUserPreference, useLayout } from '@rocket.chat/ui-contexts'; +import { useTranslation, useUserPreference, useLayout, useQuill } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; -import type { ReactElement, MouseEventHandler, FormEvent, KeyboardEventHandler, KeyboardEvent, ClipboardEventHandler } from 'react'; -import React, { memo, useRef, useReducer, useCallback } from 'react'; +import type { ReactElement, MouseEventHandler, KeyboardEventHandler, KeyboardEvent, Ref, ClipboardEventHandler } from 'react'; +import React, { memo, useRef, useCallback, useState } from 'react'; import { useSubscription } from 'use-subscription'; import { createComposerAPI } from '../../../../../app/ui-message/client/messageBox/createComposerAPI'; @@ -26,7 +27,6 @@ import { useEnablePopupPreview } from '../../../../../app/ui-message/client/popu import { getImageExtensionFromMime } from '../../../../../lib/getImageExtensionFromMime'; import { useFormatDateAndTime } from '../../../../hooks/useFormatDateAndTime'; import { useReactiveValue } from '../../../../hooks/useReactiveValue'; -import type { ComposerAPI } from '../../../../lib/chats/ChatAPI'; import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; import { keyCodes } from '../../../../lib/utils/keyCodes'; import AudioMessageRecorder from '../../../composer/AudioMessageRecorder'; @@ -39,21 +39,13 @@ import { useAutoGrow } from '../RoomComposer/hooks/useAutoGrow'; import { useMessageComposerMergedRefs } from '../hooks/useMessageComposerMergedRefs'; import MessageBoxActionsToolbar from './MessageBoxActionsToolbar'; import MessageBoxFormattingToolbar from './MessageBoxFormattingToolbar'; +import { customIcons } from './MessageBoxFormattingToolbar/MessageBoxFormattingIcons'; import MessageBoxReplies from './MessageBoxReplies'; import { useMessageBoxAutoFocus } from './hooks/useMessageBoxAutoFocus'; import { useMessageBoxPlaceholder } from './hooks/useMessageBoxPlaceholder'; +import './MessageEditor.css'; -const reducer = (_: unknown, event: FormEvent): boolean => { - const target = event.target as HTMLInputElement; - - return Boolean(target.value.trim()); -}; - -const handleFormattingShortcut = ( - event: KeyboardEvent, - formattingButtons: FormattingButton[], - composer: ComposerAPI, -) => { +const handleFormattingShortcut = (event: KeyboardEvent, formattingButtons: FormattingButton[]) => { const isMacOS = navigator.platform.indexOf('Mac') !== -1; const isCmdOrCtrlPressed = (isMacOS && event.metaKey) || (!isMacOS && event.ctrlKey); @@ -69,7 +61,6 @@ const handleFormattingShortcut = ( return false; } - composer.wrapSelection(formatter.pattern); return true; }; @@ -114,7 +105,7 @@ const MessageBox = ({ const t = useTranslation(); const composerPlaceholder = useMessageBoxPlaceholder(t('Message'), room); - const [typing, setTyping] = useReducer(reducer, false); + const [typing, setTyping] = useState(false); const { isMobile } = useLayout(); const sendOnEnterBehavior = useUserPreference<'normal' | 'alternative' | 'desktop'>('sendOnEnter') || isMobile; @@ -124,6 +115,17 @@ const MessageBox = ({ throw new Error('Chat context not found'); } + const { quillRef, quill } = useQuill({ + placeholder: composerPlaceholder, + customIcons, + }); + + console.log(window.getSelection()?.getRangeAt(0), 'idhr'); + + quill?.on('text-change', () => { + setTyping(quill.root.innerText.length !== 0 && quill.root.innerText !== '\n'); + }); + const textareaRef = useRef(null); const messageComposerRef = useRef(null); const shadowRef = useRef(null); @@ -131,7 +133,7 @@ const MessageBox = ({ const storageID = `messagebox_${room._id}${tmid ? `-${tmid}` : ''}`; const callbackRef = useCallback( - (node: HTMLTextAreaElement) => { + (node: any) => { if (node === null) { return; } @@ -153,10 +155,11 @@ const MessageBox = ({ } const ref = messageComposerRef.current as HTMLElement; - chat.emojiPicker.open(ref, (emoji: string) => chat.composer?.insertText(` :${emoji}: `)); + chat.emojiPicker.open(ref, (_emoji: string, image?: string) => chat.composer?.insertTextV2(` ${image}`)); }); const handleSendMessage = useMutableCallback(() => { + chat.composer?.wrapSelectionV2(); const text = chat.composer?.text ?? ''; chat.composer?.clear(); clearPopup(); @@ -171,7 +174,7 @@ const MessageBox = ({ const handler: KeyboardEventHandler = useMutableCallback((event) => { const { which: keyCode } = event; - const input = event.target as HTMLTextAreaElement; + const input = event.target as any; const isSubmitKey = keyCode === keyCodes.CARRIAGE_RETURN || keyCode === keyCodes.NEW_LINE; @@ -188,7 +191,7 @@ const MessageBox = ({ return false; } - if (chat.composer && handleFormattingShortcut(event, [...formattingButtons], chat.composer)) { + if (chat.composer && handleFormattingShortcut(event, [...formattingButtons])) { return; } @@ -231,14 +234,15 @@ const MessageBox = ({ } case 'ArrowDown': { - if (input.selectionEnd === input.value.length) { + console.log(input.selectionEnd, input.innerText.length); + if (input.selectionEnd === input.innerText.length) { event.preventDefault(); event.stopPropagation(); onNavigateToNextMessage?.(); if (event.altKey) { - input.setSelectionRange(input.value.length, input.value.length); + input.setSelectionRange(input.innerText.length, input.innerText.length); } } } @@ -333,15 +337,15 @@ const MessageBox = ({ ariaActiveDescendant, suspended, select, + clearPopup, commandsRef, callbackRef: c, filter, - clearPopup, } = useComposerBoxPopup<{ _id: string; sort?: number }>({ configurations: composerPopupConfig, }); - const mergedRefs = useMessageComposerMergedRefs(c, textareaRef, callbackRef, autofocusRef); + const mergedRefs = useMessageComposerMergedRefs(c, quillRef, callbackRef, autofocusRef); const shouldPopupPreview = useEnablePopupPreview(filter, popup); @@ -381,11 +385,10 @@ const MessageBox = ({ {isRecordingAudio && } } aria-label={composerPlaceholder} name='msg' disabled={isRecording || !canSend} - onChange={setTyping} style={textAreaStyle} placeholder={composerPlaceholder} onKeyDown={handler} diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/FormattingToolbarDropdown.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/FormattingToolbarDropdown.tsx index 880bc3f93284..f59107f59416 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/FormattingToolbarDropdown.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/FormattingToolbarDropdown.tsx @@ -7,11 +7,11 @@ import type { ComposerAPI } from '../../../../../lib/chats/ChatAPI'; import { useDropdownVisibility } from '../../../../../sidebar/header/hooks/useDropdownVisibility'; type FormattingToolbarDropdownProps = { - composer: ComposerAPI; + composer?: ComposerAPI; items: FormattingButton[]; }; -const FormattingToolbarDropdown = ({ composer, items, ...props }: FormattingToolbarDropdownProps) => { +const FormattingToolbarDropdown = ({ composer: _, items, ...props }: FormattingToolbarDropdownProps) => { const t = useTranslation(); const reference = useRef(null); const target = useRef(null); @@ -28,9 +28,9 @@ const FormattingToolbarDropdown = ({ composer, items, ...props }: FormattingTool const handleFormattingAction = () => { if ('link' in formatter) { window.open(formatter.link, '_blank', 'rel=noreferrer noopener'); - return; + // return; } - composer.wrapSelection(formatter.pattern); + // composer.wrapSelectionV2(formatter.pattern); }; return ( diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/MessageBoxFormattingIcons.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/MessageBoxFormattingIcons.ts new file mode 100644 index 000000000000..7943b2a2c17f --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/MessageBoxFormattingIcons.ts @@ -0,0 +1,7 @@ +export const customIcons = { + 'bold': ``, + 'italic': ``, + 'strike': ``, + 'code': ``, + 'code-block': ``, +}; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/MessageBoxFormattingToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/MessageBoxFormattingToolbar.tsx index fb8d997aaa1f..65af7aaf3d55 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/MessageBoxFormattingToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/MessageBoxFormattingToolbar.tsx @@ -1,3 +1,4 @@ +import { Box } from '@rocket.chat/fuselage'; import { MessageComposerAction } from '@rocket.chat/ui-composer'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { memo } from 'react'; @@ -16,30 +17,27 @@ type MessageBoxFormattingToolbarProps = { const MessageBoxFormattingToolbar = ({ items, variant = 'large', composer, ...props }: MessageBoxFormattingToolbarProps) => { const t = useTranslation(); - if (variant === 'small') { + if (variant === 'large') { const collapsedItems = [...items]; const featuredFormatter = collapsedItems.splice(0, 1)[0]; return ( - <> + {'icon' in featuredFormatter && ( - composer.wrapSelection(featuredFormatter.pattern)} - icon={featuredFormatter.icon} - /> + )} - + ); } return ( - <> - {items.map((formatter) => + + {items.slice(0, 5).map((formatter) => 'icon' in formatter ? ( { if ('link' in formatter) { window.open(formatter.link, '_blank', 'rel=noreferrer noopener'); - return; } - composer.wrapSelection(formatter.pattern); }} /> ) : ( @@ -66,7 +62,7 @@ const MessageBoxFormattingToolbar = ({ items, variant = 'large', composer, ...pr ), )} - + ); }; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageEditor.css b/apps/meteor/client/views/room/composer/messageBox/MessageEditor.css new file mode 100644 index 000000000000..6fd65f7d0e54 --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/MessageEditor.css @@ -0,0 +1,201 @@ + .ql-container { + box-sizing: border-box; + font-family: Helvetica, Arial, sans-serif; + font-size: 13px; + height: 100%; + margin: 0px; + position: relative; +} +.ql-clipboard { + left: -100000px; + height: 1px; + overflow-y: hidden; + position: absolute; + top: 50%; +} +.ql-clipboard p { + margin: 0; + padding: 0; +} +.ql-editor { + box-sizing: border-box; + line-height: 1.42; + height: 100%; + outline: none; + overflow-y: auto; + padding: 12px 15px; + tab-size: 4; + -moz-tab-size: 4; + text-align: left; + white-space: pre-wrap; + word-wrap: break-word; +} +.ql-editor > * { + cursor: text; +} +.ql-editor ol > li, +.ql-editor ul > li { + list-style-type: none; +} +.ql-editor ul > li::before { + content: '\2022'; +} +.ql-editor ul[data-checked=true], +.ql-editor ul[data-checked=false] { + pointer-events: none; +} +.ql-editor ul[data-checked=true] > li *, +.ql-editor ul[data-checked=false] > li * { + pointer-events: all; +} +.ql-editor ul[data-checked=true] > li::before, +.ql-editor ul[data-checked=false] > li::before { + color: #777; + cursor: pointer; + pointer-events: all; +} +.ql-editor ul[data-checked=true] > li::before { + content: '\2611'; +} +.ql-editor ul[data-checked=false] > li::before { + content: '\2610'; +} +.ql-editor li::before { + display: inline-block; + white-space: nowrap; + width: 1.2em; +} +.ql-editor li:not(.ql-direction-rtl)::before { + margin-left: -1.5em; + margin-right: 0.3em; + text-align: right; +} +.ql-editor li.ql-direction-rtl::before { + margin-left: 0.3em; + margin-right: -1.5em; +} +.ql-editor ol li:not(.ql-direction-rtl), +.ql-editor ul li:not(.ql-direction-rtl) { + padding-left: 1.5em; +} +.ql-editor ol li.ql-direction-rtl, +.ql-editor ul li.ql-direction-rtl { + padding-right: 1.5em; +} +.ql-editor ol li { + counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9; + counter-increment: list-0; +} +.ql-editor ol li:before { + content: counter(list-0, decimal) '. '; +} +.ql-editor.ql-blank::before { + color: rgba(0,0,0,0.6); + content: attr(data-placeholder); + left: 15px; + pointer-events: none; + position: absolute; + right: 15px; +} +.ql-snow.ql-toolbar:after, +.ql-snow .ql-toolbar:after { + clear: both; + content: ''; + display: table; +} +.ql-snow.ql-toolbar button, +.ql-snow .ql-toolbar button { + background: none; + border: none; + cursor: pointer; + display: inline-block; + float: left; + height: 24px; + padding: 3px 5px; + width: 28px; +} +.ql-snow.ql-toolbar button svg, +.ql-snow .ql-toolbar button svg { + float: left; + height: 100%; +} +.ql-snow.ql-toolbar button:hover .ql-fill, +.ql-snow.ql-toolbar button:focus .ql-fill, +.ql-snow.ql-toolbar button.ql-active .ql-fill, +.ql-snow.ql-toolbar .ql-picker-label:hover .ql-fill, +.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-fill, +.ql-snow.ql-toolbar .ql-picker-item:hover .ql-fill, +.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-fill, +.ql-snow.ql-toolbar button:hover .ql-stroke.ql-fill, +.ql-snow.ql-toolbar button:focus .ql-stroke.ql-fill, +.ql-snow.ql-toolbar button.ql-active .ql-stroke.ql-fill, +.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill, +.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill, +.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill, +.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill { + fill: #06c; +} +.ql-snow.ql-toolbar button:hover .ql-stroke, +.ql-snow.ql-toolbar button:focus .ql-stroke, +.ql-snow.ql-toolbar button.ql-active .ql-stroke, +.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke, +.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke, +.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke, +.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke, +.ql-snow.ql-toolbar button:hover .ql-stroke-miter, +.ql-snow.ql-toolbar button:focus .ql-stroke-miter, +.ql-snow.ql-toolbar button.ql-active .ql-stroke-miter, +.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke-miter, +.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter, +.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke-miter, +.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter { + stroke: #06c; +} +.ql-snow { + box-sizing: border-box; +} +.ql-snow * { + box-sizing: border-box; +} +.ql-snow .ql-hidden { + display: none; +} +.ql-snow .ql-stroke { + fill: none; + stroke: #444; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2; +} +.ql-snow .ql-stroke-miter { + fill: none; + stroke: #444; + stroke-miterlimit: 10; + stroke-width: 2; +} +.ql-snow .ql-fill, +.ql-snow .ql-stroke.ql-fill { + fill: #444; +} +.ql-snow .ql-editor code, +.ql-snow .ql-editor pre { + background-color: #f0f0f0; + border-radius: 3px; +} +.ql-snow .ql-editor pre { + white-space: pre-wrap; + margin-bottom: 5px; + margin-top: 5px; + padding: 5px 10px; +} +.ql-snow .ql-editor code { + font-size: 85%; + padding: 2px 4px; +} +.ql-snow .ql-editor pre.ql-syntax { + background-color: #E4E7EA; + overflow: visible; +} +.ql-container.ql-snow { + border: 1px solid #ccc; +} diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 5c26b2af58c1..e941e05a38c1 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -285,6 +285,7 @@ "@types/lodash.debounce": "^4.0.8", "@types/object-path": "^0.11.3", "@types/proxy-from-env": "^1.0.3", + "@types/quill": "^2.0.11", "@types/speakeasy": "^2.0.9", "@xmldom/xmldom": "^0.8.10", "adm-zip": "0.5.10", @@ -394,6 +395,7 @@ "psl": "^1.8.0", "query-string": "^7.1.3", "queue-fifo": "^0.2.6", + "quill": "^1.3.7", "rc-scrollbars": "^1.1.6", "react": "~17.0.2", "react-aria": "~3.23.1", diff --git a/packages/ui-client/src/components/EmojiPicker/EmojiPickerPreview.tsx b/packages/ui-client/src/components/EmojiPicker/EmojiPickerPreview.tsx index dd6a15a468b0..75c68ddd1eee 100644 --- a/packages/ui-client/src/components/EmojiPicker/EmojiPickerPreview.tsx +++ b/packages/ui-client/src/components/EmojiPicker/EmojiPickerPreview.tsx @@ -9,7 +9,6 @@ const EmojiPickerPreview = ({ emoji, name, ...props }: { emoji: string; name: st height: 40px; } `; - return ( diff --git a/packages/ui-composer/src/MessageComposer/MessageComposerInput.tsx b/packages/ui-composer/src/MessageComposer/MessageComposerInput.tsx index 870a633f9e50..0c95a03267f8 100644 --- a/packages/ui-composer/src/MessageComposer/MessageComposerInput.tsx +++ b/packages/ui-composer/src/MessageComposer/MessageComposerInput.tsx @@ -28,11 +28,8 @@ const MessageComposerInput = forwardRef(function MessageComposerInput( rows={1} fontScale='p2' ref={ref} - pi={12} - mb={16} + mb={5} borderWidth={0} - is='textarea' - {...props} /> ); diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index 7317e882d826..3fc34fb0c6d5 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -8,6 +8,7 @@ "@rocket.chat/fuselage-hooks": "~0.32.1", "@rocket.chat/rest-typings": "workspace:^", "@types/jest": "~29.5.7", + "@types/quill": "^2.0.11", "@types/react": "~17.0.69", "@types/react-dom": "~17.0.22", "@types/use-sync-external-store": "^0.0.5", @@ -15,6 +16,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "jest": "~29.6.4", "mongodb": "^4.17.1", + "quill": "^1.3.7", "react": "~17.0.2", "ts-jest": "~29.1.1", "typescript": "~5.3.2", diff --git a/packages/ui-contexts/src/hooks/useQuill.ts b/packages/ui-contexts/src/hooks/useQuill.ts new file mode 100644 index 000000000000..cea38d6fed09 --- /dev/null +++ b/packages/ui-contexts/src/hooks/useQuill.ts @@ -0,0 +1,71 @@ +/* eslint-disable prefer-rest-params */ +import type Quill from 'quill'; +import type { RefObject } from 'react'; +import { useRef, useState, useEffect } from 'react'; + +type QuillOptions = { + placeholder?: string; + customIcons?: { [key: string]: string }; +}; + +function assign(target: any, _varArgs: any) { + if (target === null || target === undefined) { + throw new TypeError('Cannot convert undefined or null to object'); + } + + const to = Object(target); + + for (let index = 1; index < arguments.length; index++) { + const nextSource = arguments[index]; + + if (nextSource !== null && nextSource !== undefined) { + for (const nextKey in nextSource) { + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; +} + +const setIcons = (obj: any, options: QuillOptions) => { + const icons = obj.Quill.import('ui/icons'); + const customIcons = options.customIcons || {}; + + Object.keys(customIcons).forEach((key) => { + icons[key] = customIcons[key]; + }); +}; + +export const useQuill = (options: QuillOptions) => { + const quillRef: RefObject = useRef(); + + const [isLoaded, setIsLoaded] = useState(false); + const [obj, setObj] = useState({ + Quill: undefined as any | undefined, + quillRef, + quill: undefined as Quill | undefined, + }); + + useEffect(() => { + if (!obj.Quill) { + setObj((prev) => assign(prev, { Quill: require('quill') })); + } + if (obj.Quill && !obj.quill && quillRef && quillRef.current && isLoaded) { + setIcons(obj, options); + const opts = { + modules: { toolbar: '#toolbar' }, + placeholder: options.placeholder, + formats: ['bold', 'italic', 'underline', 'strike', 'list', 'code', 'code-block'], + theme: 'snow', + }; + const quill = new obj.Quill(quillRef.current, opts); + + setObj(assign(assign({}, obj), { quill })); + } + setIsLoaded(true); + }, [isLoaded, options, obj]); + + return obj; +}; diff --git a/packages/ui-contexts/src/index.ts b/packages/ui-contexts/src/index.ts index 4e35d9e02af8..51b245b76d5d 100644 --- a/packages/ui-contexts/src/index.ts +++ b/packages/ui-contexts/src/index.ts @@ -49,6 +49,7 @@ export { useMethod } from './hooks/useMethod'; export { useModal } from './hooks/useModal'; export { usePermission } from './hooks/usePermission'; export { usePermissionWithScopedRoles } from './hooks/usePermissionWithScopedRoles'; +export { useQuill } from './hooks/useQuill'; export { useRole } from './hooks/useRole'; export { useRolesDescription } from './hooks/useRolesDescription'; export { useRoomAvatarPath } from './hooks/useRoomAvatarPath'; diff --git a/yarn.lock b/yarn.lock index da83e044e00f..e710700cb8da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9561,6 +9561,7 @@ __metadata: "@types/proxy-from-env": ^1.0.3 "@types/proxyquire": ^1.3.30 "@types/psl": ^1.1.2 + "@types/quill": ^2.0.11 "@types/react": ~17.0.69 "@types/react-dom": ~17.0.22 "@types/rewire": ^2.5.29 @@ -9729,6 +9730,7 @@ __metadata: psl: ^1.8.0 query-string: ^7.1.3 queue-fifo: ^0.2.6 + quill: ^1.3.7 rc-scrollbars: ^1.1.6 react: ~17.0.2 react-aria: ~3.23.1 @@ -10376,6 +10378,7 @@ __metadata: "@rocket.chat/password-policies": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@types/jest": ~29.5.7 + "@types/quill": ^2.0.11 "@types/react": ~17.0.69 "@types/react-dom": ~17.0.22 "@types/use-sync-external-store": ^0.0.5 @@ -10383,6 +10386,7 @@ __metadata: eslint-plugin-react-hooks: ^4.6.0 jest: ~29.6.4 mongodb: ^4.17.1 + quill: ^1.3.7 react: ~17.0.2 ts-jest: ~29.1.1 typescript: ~5.3.2 @@ -13832,6 +13836,16 @@ __metadata: languageName: node linkType: hard +"@types/quill@npm:^2.0.11": + version: 2.0.11 + resolution: "@types/quill@npm:2.0.11" + dependencies: + parchment: ^1.1.2 + quill-delta: ^4.0.1 + checksum: c4bb373c9fa075938b31778c2ccf67338babe0ccdaac68034933ba74100596b56d245d1a065d320a743255af7c0880870a78657c103be5d3c8caaf8ad7b5363c + languageName: node + linkType: hard + "@types/range-parser@npm:*": version: 1.2.4 resolution: "@types/range-parser@npm:1.2.4" @@ -18229,7 +18243,7 @@ __metadata: languageName: node linkType: hard -"clone@npm:^2.1.2": +"clone@npm:^2.1.1, clone@npm:^2.1.2": version: 2.1.2 resolution: "clone@npm:2.1.2" checksum: aaf106e9bc025b21333e2f4c12da539b568db4925c0501a1bf4070836c9e848c892fa22c35548ce0d1132b08bbbfa17a00144fe58fccdab6fa900fec4250f67d @@ -20009,6 +20023,20 @@ __metadata: languageName: node linkType: hard +"deep-equal@npm:^1.0.1": + version: 1.1.1 + resolution: "deep-equal@npm:1.1.1" + dependencies: + is-arguments: ^1.0.4 + is-date-object: ^1.0.1 + is-regex: ^1.0.4 + object-is: ^1.0.1 + object-keys: ^1.1.1 + regexp.prototype.flags: ^1.2.0 + checksum: f92686f2c5bcdf714a75a5fa7a9e47cb374a8ec9307e717b8d1ce61f56a75aaebf5619c2a12b8087a705b5a2f60d0292c35f8b58cb1f72e3268a3a15cab9f78d + languageName: node + linkType: hard + "deep-equal@npm:^2.0.5": version: 2.2.2 resolution: "deep-equal@npm:2.2.2" @@ -21916,6 +21944,13 @@ __metadata: languageName: node linkType: hard +"eventemitter3@npm:^2.0.3": + version: 2.0.3 + resolution: "eventemitter3@npm:2.0.3" + checksum: dfbf4a07144afea0712d8e6a7f30ae91beb7c12c36c3d480818488aafa437d9a331327461f82c12dfd60a4fbad502efc97f684089cda02809988b84a23630752 + languageName: node + linkType: hard + "eventemitter3@npm:^3.1.0": version: 3.1.2 resolution: "eventemitter3@npm:3.1.2" @@ -22294,7 +22329,14 @@ __metadata: languageName: node linkType: hard -"fast-diff@npm:^1.1.2": +"fast-diff@npm:1.1.2": + version: 1.1.2 + resolution: "fast-diff@npm:1.1.2" + checksum: 2ef726603e22a89ef27225bfaef24c17e3aec188df24da4629d5f012b23a884e09a0c7299ff37a0aec7aa788755bd554f5801f698de4deeffce83308bd11405d + languageName: node + linkType: hard + +"fast-diff@npm:1.2.0, fast-diff@npm:^1.1.2": version: 1.2.0 resolution: "fast-diff@npm:1.2.0" checksum: 1b5306eaa9e826564d9e5ffcd6ebd881eb5f770b3f977fcbf38f05c824e42172b53c79920e8429c54eb742ce15a0caf268b0fdd5b38f6de52234c4a8368131ae @@ -26353,7 +26395,7 @@ __metadata: languageName: node linkType: hard -"is-regex@npm:^1.1.2, is-regex@npm:^1.1.4": +"is-regex@npm:^1.0.4, is-regex@npm:^1.1.2, is-regex@npm:^1.1.4": version: 1.1.4 resolution: "is-regex@npm:1.1.4" dependencies: @@ -31792,6 +31834,13 @@ __metadata: languageName: node linkType: hard +"parchment@npm:^1.1.2, parchment@npm:^1.1.4": + version: 1.1.4 + resolution: "parchment@npm:1.1.4" + checksum: 47997567424d1ad8648046091a06b3a5423ed83f9dfa421d7fd93e0032e79aedd8db5499ff55327e08eebd89d8e927704a646f87d45d4bcfe63016aa5a88947d + languageName: node + linkType: hard + "parent-module@npm:^1.0.0": version: 1.0.1 resolution: "parent-module@npm:1.0.1" @@ -33978,6 +34027,42 @@ __metadata: languageName: node linkType: hard +"quill-delta@npm:^3.6.2": + version: 3.6.3 + resolution: "quill-delta@npm:3.6.3" + dependencies: + deep-equal: ^1.0.1 + extend: ^3.0.2 + fast-diff: 1.1.2 + checksum: e62ed339838077841db401da3181bdf559c6667d014a671767788380c5be13a6205603bcdd27445260e6f6b2b5519161e1000023e521e3b2ff087270fa67fef6 + languageName: node + linkType: hard + +"quill-delta@npm:^4.0.1": + version: 4.2.2 + resolution: "quill-delta@npm:4.2.2" + dependencies: + fast-diff: 1.2.0 + lodash.clonedeep: ^4.5.0 + lodash.isequal: ^4.5.0 + checksum: 6a71e3dfb46dfdc80504b5ac961c17c5ba7b576ca1240b0e212598ce5be1d510a9457531832b15ea46a0261867444faca1c1fe451b26b81b6158852e00d7422f + languageName: node + linkType: hard + +"quill@npm:^1.3.7": + version: 1.3.7 + resolution: "quill@npm:1.3.7" + dependencies: + clone: ^2.1.1 + deep-equal: ^1.0.1 + eventemitter3: ^2.0.3 + extend: ^3.0.2 + parchment: ^1.1.4 + quill-delta: ^3.6.2 + checksum: db3e265a8410a4554e50a18cae4ebc0b43a996a776bcf03e26abcadbf617f4db329d49a0fa3ada6a70538a369bbbdc8fa7a66086f194b481914bf1adbab16f8f + languageName: node + linkType: hard + "raf-schd@npm:^4.0.2": version: 4.0.3 resolution: "raf-schd@npm:4.0.3" @@ -35065,7 +35150,7 @@ __metadata: languageName: node linkType: hard -"regexp.prototype.flags@npm:^1.4.3, regexp.prototype.flags@npm:^1.5.0, regexp.prototype.flags@npm:^1.5.1": +"regexp.prototype.flags@npm:^1.2.0, regexp.prototype.flags@npm:^1.4.3, regexp.prototype.flags@npm:^1.5.0, regexp.prototype.flags@npm:^1.5.1": version: 1.5.1 resolution: "regexp.prototype.flags@npm:1.5.1" dependencies: