Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Live preview message composer #30414

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -93,6 +94,9 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string)
};

const clear = (): void => {
while (input.childNodes[0].firstChild) {
input.childNodes[0].removeChild(input.childNodes[0].firstChild);
}
setText('');
};

Expand Down Expand Up @@ -260,6 +264,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) ?? '', {
Expand Down Expand Up @@ -314,6 +322,7 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string)
},
release,
wrapSelection,
wrapSelectionV2,
get text(): string {
return input.value;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type FormattingButton =
label: TranslationKey;
icon: IconName;
pattern: string;
buttonName?: string;
// text?: () => string | undefined;
command?: string;
link?: string;
Expand All @@ -24,28 +25,33 @@ export const formattingButtons: ReadonlyArray<FormattingButton> = [
{
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``` ',
},
{
Expand Down
62 changes: 62 additions & 0 deletions apps/meteor/app/ui-utils/client/lib/buildMarkDown.ts
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions apps/meteor/client/lib/chats/ChatAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type ComposerAPI = {
},
): void;
wrapSelection(pattern: string): void;
wrapSelectionV2(pattern?: string): void;
insertText(text: string): void;
insertNewLine(): void;
clear(): void;
Expand Down
53 changes: 27 additions & 26 deletions apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -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';
Expand All @@ -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<HTMLInputElement>): boolean => {
const target = event.target as HTMLInputElement;

return Boolean(target.value.trim());
};

const handleFormattingShortcut = (
event: KeyboardEvent<HTMLTextAreaElement>,
formattingButtons: FormattingButton[],
composer: ComposerAPI,
) => {
const handleFormattingShortcut = (event: KeyboardEvent<HTMLTextAreaElement>, formattingButtons: FormattingButton[]) => {
const isMacOS = navigator.platform.indexOf('Mac') !== -1;
const isCmdOrCtrlPressed = (isMacOS && event.metaKey) || (!isMacOS && event.ctrlKey);

Expand All @@ -69,7 +61,6 @@ const handleFormattingShortcut = (
return false;
}

composer.wrapSelection(formatter.pattern);
return true;
};

Expand Down Expand Up @@ -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;
Expand All @@ -124,14 +115,23 @@ const MessageBox = ({
throw new Error('Chat context not found');
}

const { quillRef, quill } = useQuill({
placeholder: composerPlaceholder,
customIcons,
});

quill?.on('text-change', () => {
setTyping(quill.root.innerText.length !== 0 && quill.root.innerText !== '\n');
});

const textareaRef = useRef<HTMLTextAreaElement>(null);
const messageComposerRef = useRef<HTMLElement>(null);
const shadowRef = useRef(null);

const storageID = `messagebox_${room._id}${tmid ? `-${tmid}` : ''}`;

const callbackRef = useCallback(
(node: HTMLTextAreaElement) => {
(node: any) => {
if (node === null) {
return;
}
Expand All @@ -157,6 +157,7 @@ const MessageBox = ({
});

const handleSendMessage = useMutableCallback(() => {
chat.composer?.wrapSelectionV2();
const text = chat.composer?.text ?? '';
chat.composer?.clear();
clearPopup();
Expand All @@ -171,7 +172,7 @@ const MessageBox = ({
const handler: KeyboardEventHandler<HTMLTextAreaElement> = 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;

Expand All @@ -188,7 +189,7 @@ const MessageBox = ({
return false;
}

if (chat.composer && handleFormattingShortcut(event, [...formattingButtons], chat.composer)) {
if (chat.composer && handleFormattingShortcut(event, [...formattingButtons])) {
return;
}

Expand Down Expand Up @@ -231,14 +232,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);
}
}
}
Expand Down Expand Up @@ -333,15 +335,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);

Expand Down Expand Up @@ -381,11 +383,10 @@ const MessageBox = ({
<MessageComposer ref={messageComposerRef} variant={isEditing ? 'editing' : undefined}>
{isRecordingAudio && <AudioMessageRecorder rid={room._id} isMicrophoneDenied={isMicrophoneDenied} />}
<MessageComposerInput
ref={mergedRefs}
ref={mergedRefs as unknown as Ref<any>}
aria-label={composerPlaceholder}
name='msg'
disabled={isRecording || !canSend}
onChange={setTyping}
style={textAreaStyle}
placeholder={composerPlaceholder}
onKeyDown={handler}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const customIcons = {
'bold': `<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path d="M7.29243 5.29263C7.47998 5.10509 7.73435 4.99974 7.99958 4.99976L17.5002 5.00037C17.5003 5.00037 17.5002 5.00037 17.5002 5.00037C18.9589 5.00038 20.3579 5.57984 21.3893 6.61128C22.4208 7.64273 23.0002 9.04167 23.0002 10.5004C23.0002 11.9591 22.4208 13.358 21.3893 14.3895C21.3642 14.4146 21.3388 14.4395 21.3132 14.4641C22.0278 14.7626 22.6847 15.1995 23.2429 15.7577C24.3681 16.8829 25.0002 18.4091 25.0002 20.0004C25.0002 21.5917 24.3681 23.1178 23.2429 24.243C22.1177 25.3682 20.5915 26.0004 19.0002 26.0004L7.99946 25.9998C7.44719 25.9997 6.99951 25.552 6.99951 24.9998V5.99976C6.99951 5.73453 7.10488 5.48016 7.29243 5.29263ZM17.5002 14.0004C18.4285 14.0004 19.3187 13.6316 19.9751 12.9752C20.6315 12.3189 21.0002 11.4286 21.0002 10.5004C21.0002 9.57211 20.6315 8.68187 19.9751 8.02549C19.3187 7.36911 18.4285 7.00037 17.5002 7.00037L8.99951 6.99982V14.0004H17.5002ZM8.99951 16.0004V23.9998L19.0002 24.0004C19.0003 24.0004 19.0002 24.0004 19.0002 24.0004C20.0611 24.0004 21.0785 23.5789 21.8287 22.8288C22.5788 22.0786 23.0002 21.0612 23.0002 20.0004C23.0002 18.9395 22.5788 17.9221 21.8287 17.1719C21.0785 16.4218 20.0611 16.0004 19.0002 16.0004H8.99951Z"/></svg>`,
'italic': `<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path d="M17.6122 7.99976L12.2788 23.9998H7.99939C7.44711 23.9998 6.99939 24.4475 6.99939 24.9998C6.99939 25.552 7.44711 25.9998 7.99939 25.9998H17.9994C18.5517 25.9998 18.9994 25.552 18.9994 24.9998C18.9994 24.4475 18.5517 23.9998 17.9994 23.9998H14.387L19.7204 7.99976H23.9994C24.5517 7.99976 24.9994 7.55204 24.9994 6.99976C24.9994 6.44747 24.5517 5.99976 23.9994 5.99976H19.0306C19.0098 5.9991 18.9891 5.99911 18.9684 5.99976H13.9994C13.4471 5.99976 12.9994 6.44747 12.9994 6.99976C12.9994 7.55204 13.4471 7.99976 13.9994 7.99976H17.6122Z"/></svg>`,
'strike': `<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path d="M10.3888 11C10.3888 9.01438 12.4323 7 15.9999 7C18.7071 7 20.5963 8.19929 21.2919 9.62782C21.5337 10.1244 22.1323 10.3309 22.6288 10.0891C23.1254 9.84729 23.3319 9.24875 23.0901 8.75221C21.9677 6.44723 19.2373 5 15.9999 5C11.837 5 8.39129 7.46097 8.38877 10.9961C8.38569 11.4408 8.4535 11.8832 8.58965 12.3065C8.75874 12.8323 9.32203 13.1214 9.8478 12.9523C10.3736 12.7833 10.6627 12.22 10.4936 11.6942C10.4223 11.4725 10.3869 11.2408 10.3888 11.0079V11ZM5 15C4.44772 15 4 15.4477 4 16C4 16.5523 4.44772 17 5 17H17.1558C18.5894 17.4262 19.8138 17.886 20.6833 18.5225C21.5245 19.1382 22 19.8889 22 21.0001C22 21.9741 21.449 22.9501 20.3685 23.7219C19.2899 24.4923 17.7486 25.0001 16 25.0001C14.2514 25.0001 12.7101 24.4923 11.6315 23.7219C10.551 22.9501 10 21.9741 10 21.0001C10 20.4478 9.55228 20.0001 9 20.0001C8.44772 20.0001 8 20.4478 8 21.0001C8 22.7876 9.01603 24.3115 10.469 25.3494C11.9239 26.3886 13.8826 27.0001 16 27.0001C18.1174 27.0001 20.0761 26.3886 21.531 25.3494C22.984 24.3115 24 22.7876 24 21.0001C24 19.2012 23.1802 17.9165 21.9865 17H27C27.5523 17 28 16.5523 28 16C28 15.4477 27.5523 15 27 15H17.3219C17.3072 14.9997 17.2925 14.9997 17.2779 15H5Z"/></svg>`,
'code': `<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path d="M19.6923 6.39999C20.2078 6.59825 20.4649 7.17684 20.2667 7.69232L13.6 25.0257C13.4018 25.5411 12.8232 25.7983 12.3077 25.6C11.7922 25.4018 11.5351 24.8232 11.7333 24.3077L18.4 6.97436C18.5982 6.45889 19.1768 6.20173 19.6923 6.39999ZM10.0404 11.2929C10.431 11.6834 10.431 12.3166 10.0404 12.7071L6.74755 16L10.0404 19.2929C10.431 19.6834 10.431 20.3166 10.0404 20.7071C9.64992 21.0976 9.01675 21.0976 8.62623 20.7071L4.62623 16.7071C4.2357 16.3166 4.2357 15.6834 4.62623 15.2929L8.62623 11.2929C9.01675 10.9024 9.64992 10.9024 10.0404 11.2929ZM21.9596 11.2929C22.3501 10.9024 22.9832 10.9024 23.3738 11.2929L27.3738 15.2929C27.7643 15.6834 27.7643 16.3166 27.3738 16.7071L23.3738 20.7071C22.9832 21.0976 22.3501 21.0976 21.9596 20.7071C21.569 20.3166 21.569 19.6834 21.9596 19.2929L25.2525 16L21.9596 12.7071C21.569 12.3166 21.569 11.6834 21.9596 11.2929Z"/></svg>`,
'code-block': `<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path d="M15.859 4.06668C16.3745 4.26494 16.6316 4.84353 16.4333 5.359L11.4333 18.359C11.2351 18.8745 10.6565 19.1316 10.141 18.9334C9.62555 18.7351 9.3684 18.1565 9.56665 17.641L14.5667 4.64105C14.7649 4.12557 15.3435 3.86842 15.859 4.06668Z"/><path d="M8.70711 7.79292C9.09763 8.18344 9.09763 8.81661 8.70711 9.20713L6.41421 11.5L8.70711 13.7929C9.09763 14.1834 9.09763 14.8166 8.70711 15.2071C8.31658 15.5977 7.68342 15.5977 7.29289 15.2071L4.29289 12.2071C4.10536 12.0196 4 11.7652 4 11.5C4 11.2348 4.10536 10.9805 4.29289 10.7929L7.29289 7.79292C7.68342 7.40239 8.31658 7.40239 8.70711 7.79292Z"/><path d="M17.2929 7.79292C17.6834 7.40239 18.3166 7.40239 18.7071 7.79292L21.7071 10.7929C21.8946 10.9805 22 11.2348 22 11.5C22 11.7652 21.8946 12.0196 21.7071 12.2071L18.7071 15.2071C18.3166 15.5977 17.6834 15.5977 17.2929 15.2071C16.9024 14.8166 16.9024 14.1834 17.2929 13.7929L19.5858 11.5L17.2929 9.20713C16.9024 8.81661 16.9024 8.18344 17.2929 7.79292Z"/><path d="M21 5C21 4.44772 21.4477 4 22 4H25C26.6569 4 28 5.34315 28 7V25C28 26.6569 26.6569 28 25 28H7C5.34314 28 4 26.6569 4 25V20C4 19.4477 4.44772 19 5 19C5.55228 19 6 19.4477 6 20V26H26V6H22C21.4477 6 21 5.55228 21 5Z"/></svg>`,
};
Loading