diff --git a/component/src/types/messagesInternal.ts b/component/src/types/messagesInternal.ts index a19944ad8..e6a081abf 100644 --- a/component/src/types/messagesInternal.ts +++ b/component/src/types/messagesInternal.ts @@ -1,9 +1,13 @@ import {MessageElements} from '../views/chat/messages/messages'; -import {MessageFileType} from './messageFile'; +import {MessageFile, MessageFileType} from './messageFile'; import {PropsRequired} from './utilityTypes'; import {MessageContent} from './messages'; -export type MessageToElements = [MessageContentI, MessageElements[]][]; +export type MessageBody = {text?: string; files?: MessageFile[]; html?: string}; + +export type MessageBodyElements = {text?: MessageElements; files?: MessageElements[]; html?: MessageElements}; + +export type MessageToElements = [MessageContentI, MessageBodyElements][]; export type MessageContentI = PropsRequired; diff --git a/component/src/views/chat/messages/fileMessageUtils.ts b/component/src/views/chat/messages/fileMessageUtils.ts index ff5ee0ac6..95ed4ec63 100644 --- a/component/src/views/chat/messages/fileMessageUtils.ts +++ b/component/src/views/chat/messages/fileMessageUtils.ts @@ -5,12 +5,14 @@ import {MessageElements} from './messages'; export class FileMessageUtils { public static readonly DEFAULT_FILE_NAME = 'file'; + public static readonly FILE_BUBBLE_CLASS = 'file-message'; // prettier-ignore public static addMessage( messages: MessagesBase, elements: MessageElements, styles: keyof MessageStyles, role: string, isTop: boolean) { if (styles === 'loading') return; messages.applyCustomStyles(elements, role, true, messages.messageStyles?.[styles]); + elements.bubbleElement.classList.add(FileMessageUtils.FILE_BUBBLE_CLASS); if (!isTop) { messages.elementRef.appendChild(elements.outerContainer); messages.elementRef.scrollTop = messages.elementRef.scrollHeight; diff --git a/component/src/views/chat/messages/fileMessages.ts b/component/src/views/chat/messages/fileMessages.ts index 0f539b48d..a16595cc1 100644 --- a/component/src/views/chat/messages/fileMessages.ts +++ b/component/src/views/chat/messages/fileMessages.ts @@ -7,6 +7,10 @@ import {MessageUtils} from './messageUtils'; import {Messages} from './messages'; export class FileMessages { + private static readonly IMAGE_BUBBLE_CLASS = 'image-message'; + private static readonly AUDIO_BUBBLE_CLASS = 'audio-message'; + private static readonly ANY_FILE_BUBBLE_CLASS = 'any-file-message'; + private static createImage(imageData: MessageFile, messagesContainerEl: HTMLElement, isTop: boolean) { const imageElement = new Image(); imageElement.src = imageData.src as string; @@ -19,7 +23,7 @@ export class FileMessages { const image = FileMessages.createImage(imageData, messages.elementRef, isTop); const elements = messages.createNewMessageElement('', role); elements.bubbleElement.appendChild(image); - elements.bubbleElement.classList.add('image-message'); + elements.bubbleElement.classList.add(FileMessages.IMAGE_BUBBLE_CLASS); FileMessageUtils.addMessage(messages, elements, 'image', role, isTop); } @@ -41,7 +45,7 @@ export class FileMessages { const audioElement = FileMessages.createAudioElement(audioData, role); const elements = messages.createMessageElementsOnOrientation('', role, isTop); elements.bubbleElement.appendChild(audioElement); - elements.bubbleElement.classList.add('audio-message'); + elements.bubbleElement.classList.add(FileMessages.AUDIO_BUBBLE_CLASS); FileMessageUtils.addMessage(messages, elements, 'audio', role, isTop); } @@ -64,7 +68,7 @@ export class FileMessages { private static addNewAnyFileMessage(messages: Messages, data: MessageFile, role: string, isTop: boolean) { const elements = messages.createMessageElementsOnOrientation('', role, isTop); const anyFile = FileMessages.createAnyFile(data); - elements.bubbleElement.classList.add('any-file-message-bubble'); + elements.bubbleElement.classList.add(FileMessages.ANY_FILE_BUBBLE_CLASS); elements.bubbleElement.appendChild(anyFile); FileMessageUtils.addMessage(messages, elements, 'file', role, isTop); } diff --git a/component/src/views/chat/messages/history/history.ts b/component/src/views/chat/messages/history/history.ts index c35cf799d..aa05d9249 100644 --- a/component/src/views/chat/messages/history/history.ts +++ b/component/src/views/chat/messages/history/history.ts @@ -42,8 +42,8 @@ export class History { if (message) { const messageContent = addAnyMessage({...message, sendUpdate: true}, true, true); if (messageContent) { - const num = MessageUtils.getNumberOfElements(messageContent); - messageToElements.unshift([messageContent, messageElementRefs.slice(0, num)]); + const messageBody = MessageUtils.generateMessageBody(messageContent, messageElementRefs); + messageToElements.unshift([messageContent, messageBody]); } return messageContent; } else { diff --git a/component/src/views/chat/messages/html/htmlMessages.ts b/component/src/views/chat/messages/html/htmlMessages.ts index 854fc3bd9..e85965979 100644 --- a/component/src/views/chat/messages/html/htmlMessages.ts +++ b/component/src/views/chat/messages/html/htmlMessages.ts @@ -6,6 +6,8 @@ import {MessageElements} from '../messages'; import {HTMLUtils} from './htmlUtils'; export class HTMLMessages { + public static readonly HTML_BUBBLE_CLASS = 'html-message'; + private static addElement(messages: MessagesBase, outerElement: HTMLElement) { messages.elementRef.appendChild(outerElement); messages.elementRef.scrollTop = messages.elementRef.scrollHeight; @@ -13,14 +15,16 @@ export class HTMLMessages { public static createElements(messages: MessagesBase, html: string, role: string, isTop: boolean) { const messageElements = messages.createMessageElementsOnOrientation('', role, isTop); - messageElements.bubbleElement.classList.add('html-message'); + messageElements.bubbleElement.classList.add(HTMLMessages.HTML_BUBBLE_CLASS); messageElements.bubbleElement.innerHTML = html; return messageElements; } + // prettier-ignore private static overwrite(messages: MessagesBase, html: string, role: string, messagesEls: MessageElements[]) { const {messageToElements: msgToEls} = messages; - const overwrittenElements = MessageUtils.overwriteMessage(msgToEls, messagesEls, html, role, 'html', 'html-message'); + const overwrittenElements = MessageUtils.overwriteMessage( + msgToEls, messagesEls, html, role, 'html', HTMLMessages.HTML_BUBBLE_CLASS); if (overwrittenElements) { overwrittenElements.bubbleElement.innerHTML = html; HTMLUtils.apply(messages, overwrittenElements.outerContainer); diff --git a/component/src/views/chat/messages/messageUtils.ts b/component/src/views/chat/messages/messageUtils.ts index b22a6d03a..7bec53117 100644 --- a/component/src/views/chat/messages/messageUtils.ts +++ b/component/src/views/chat/messages/messageUtils.ts @@ -1,7 +1,10 @@ -import {MessageContentI, MessageToElements} from '../../../types/messagesInternal'; +import {MessageBodyElements, MessageContentI, MessageToElements} from '../../../types/messagesInternal'; import {LoadingStyle} from '../../../utils/loading/loadingStyle'; import {MessageContent} from '../../../types/messages'; +import {FileMessageUtils} from './fileMessageUtils'; +import {HTMLMessages} from './html/htmlMessages'; import {Avatars} from '../../../types/avatars'; +import {MessagesBase} from './messagesBase'; import {MessageElements} from './messages'; import {Names} from '../../../types/names'; import {Avatar} from './avatar'; @@ -133,7 +136,7 @@ export class MessageUtils { messageEls.outerContainer.classList.add(...classes); } - public static getNumberOfElements(messageContent: MessageContentI) { + private static getNumberOfElements(messageContent: MessageContentI) { let length = 0; if (messageContent.text !== undefined) length += 1; if (messageContent.html !== undefined) length += 1; @@ -141,6 +144,30 @@ export class MessageUtils { return length; } + private static filterdMessageElements(elements: MessageElements[], className: string) { + return elements.filter((msgElements) => msgElements.bubbleElement.classList.contains(className)); + } + + private static findMessageElements(elements: MessageElements[], className: string) { + return elements.find((msgElements) => msgElements.bubbleElement.classList.contains(className)); + } + + private static generateMessageBodyElements(messageContent: MessageContentI, elements: MessageElements[]) { + const msgBodyEls: MessageBodyElements = {}; + if (messageContent.text) msgBodyEls.text = MessageUtils.findMessageElements(elements, MessagesBase.TEXT_BUBBLE_CLASS); + if (messageContent.html) msgBodyEls.html = MessageUtils.findMessageElements(elements, HTMLMessages.HTML_BUBBLE_CLASS); + if (messageContent.files) { + msgBodyEls.files = MessageUtils.filterdMessageElements(elements, FileMessageUtils.FILE_BUBBLE_CLASS); + } + return msgBodyEls; + } + + public static generateMessageBody(messageContent: MessageContentI, messageElementRefs: MessageElements[]) { + const numberOfMessageContentElement = MessageUtils.getNumberOfElements(messageContent); + const elements = messageElementRefs.slice(messageElementRefs.length - numberOfMessageContentElement); + return MessageUtils.generateMessageBodyElements(messageContent, elements); + } + public static classifyMessages(role: string, messageElementRefs: MessageElements[]) { const currentRole = MessageUtils.buildRoleContainerClass(role); messageElementRefs.forEach((messageEls, index) => { diff --git a/component/src/views/chat/messages/messages.css b/component/src/views/chat/messages/messages.css index af8aa9c1f..524fa23ef 100644 --- a/component/src/views/chat/messages/messages.css +++ b/component/src/views/chat/messages/messages.css @@ -144,7 +144,7 @@ pre { float: right; } -.any-file-message-bubble { +.any-file-message { padding: 1px; } diff --git a/component/src/views/chat/messages/messages.ts b/component/src/views/chat/messages/messages.ts index 9f4b8b208..e69f114dd 100644 --- a/component/src/views/chat/messages/messages.ts +++ b/component/src/views/chat/messages/messages.ts @@ -179,8 +179,8 @@ export class Messages extends MessagesBase { private updateStateOnMessage(messageContent: MessageContentI, overwritten?: boolean, update = true, isHistory = false) { if (!overwritten) { - const num = MessageUtils.getNumberOfElements(messageContent); - this.messageToElements.push([messageContent, this.messageElementRefs.slice(this.messageElementRefs.length - num)]); + const messageBody = MessageUtils.generateMessageBody(messageContent, this.messageElementRefs); + this.messageToElements.push([messageContent, messageBody]); } if (update) this.sendClientUpdate(messageContent, isHistory); } @@ -338,11 +338,11 @@ export class Messages extends MessagesBase { }); this.messageElementRefs = retainedElements; const retainedMessageToElements = this.messageToElements.filter((msgToEls) => { - if (msgToEls[0].text !== undefined || msgToEls[0].html !== undefined) { - // safe because streamed messages can't contain multiple props (text, html) - return msgToEls[1].find((els) => Messages.isActiveElement(els.bubbleElement.classList)); - } - return false; + // safe because streamed messages can't contain multiple props (text, html) + return ( + (msgToEls[1].text && Messages.isActiveElement(msgToEls[1].text.bubbleElement.classList)) || + (msgToEls[1].html && Messages.isActiveElement(msgToEls[1].html.bubbleElement.classList)) + ); }); this.messageToElements.splice(0, this.messageToElements.length, ...retainedMessageToElements); if (isReset !== false) { diff --git a/component/src/views/chat/messages/messagesBase.ts b/component/src/views/chat/messages/messagesBase.ts index 34c5d3456..f08d58456 100644 --- a/component/src/views/chat/messages/messagesBase.ts +++ b/component/src/views/chat/messages/messagesBase.ts @@ -32,6 +32,7 @@ export class MessagesBase { protected readonly _names?: Names; private _remarkable: Remarkable; private readonly _onMessage?: (message: MessageContentI, isHistory: boolean) => void; + public static readonly TEXT_BUBBLE_CLASS = 'text-message'; constructor(deepChat: DeepChat) { this.elementRef = MessagesBase.createContainerElement(); @@ -42,7 +43,7 @@ export class MessagesBase { this._onMessage = FireEvents.onMessage.bind(this, deepChat); if (deepChat.htmlClassUtilities) this.htmlClassUtilities = deepChat.htmlClassUtilities; setTimeout(() => { - this.submitUserMessage = deepChat.submitUserMessage; // wait for it to be available + this.submitUserMessage = deepChat.submitUserMessage; // wait for it to be available in input.ts }); } @@ -61,14 +62,16 @@ export class MessagesBase { const messageElements = isTop ? this.createAndPrependNewMessageElement(text, role, isTop) : this.createAndAppendNewMessageElement(text, role); - messageElements.bubbleElement.classList.add('text-message'); + messageElements.bubbleElement.classList.add(MessagesBase.TEXT_BUBBLE_CLASS); this.applyCustomStyles(messageElements, role, false); MessageUtils.fillEmptyMessageElement(messageElements.bubbleElement, text); return messageElements; } + // prettier-ignore private overwriteText(role: string, text: string, elementRefs: MessageElements[]) { - const elems = MessageUtils.overwriteMessage(this.messageToElements, elementRefs, text, role, 'text', 'text-message'); + const elems = MessageUtils.overwriteMessage( + this.messageToElements, elementRefs, text, role, 'text', MessagesBase.TEXT_BUBBLE_CLASS); if (elems) this.renderText(elems.bubbleElement, text); return elems; } @@ -128,11 +131,16 @@ export class MessagesBase { return MessagesBase.isLoadingMessage(elements) || HTMLDeepChatElements.isElementTemporary(elements); } - public createMessageElements(text: string, role: string, isTop = false) { + private createElements(text: string, role: string) { const messageElements = MessagesBase.createBaseElements(role); const {outerContainer, innerContainer, bubbleElement} = messageElements; outerContainer.appendChild(innerContainer); this.addInnerContainerElements(bubbleElement, text, role); + return messageElements; + } + + public createMessageElements(text: string, role: string, isTop = false) { + const messageElements = this.createElements(text, role); MessageUtils.updateRefArr(this.messageElementRefs, messageElements, isTop); MessageUtils.classifyMessages(role, this.messageElementRefs); return messageElements; @@ -218,8 +226,7 @@ export class MessagesBase { protected refreshTextMessages() { this._remarkable = RemarkableConfig.createNew(); this.messageToElements.forEach((msgToEls) => { - // important for text bubble to be first if multiple message content properties are used - if (msgToEls[0].text) this.renderText(msgToEls[1][0].bubbleElement, msgToEls[0].text); + if (msgToEls[1].text && msgToEls[0].text) this.renderText(msgToEls[1].text.bubbleElement, msgToEls[0].text); }); } } diff --git a/component/src/views/chat/messages/stream/messageStream.ts b/component/src/views/chat/messages/stream/messageStream.ts index 1f135b81f..7c09e27ac 100644 --- a/component/src/views/chat/messages/stream/messageStream.ts +++ b/component/src/views/chat/messages/stream/messageStream.ts @@ -55,7 +55,7 @@ export class MessageStream { this._elements.bubbleElement.classList.add(MessageStream.MESSAGE_CLASS); this._activeMessageRole = role; this._message = {role: this._activeMessageRole, [streamType]: content}; - this._messages.messageToElements.push([this._message, [this._elements]]); + this._messages.messageToElements.push([this._message, {[streamType]: this._elements}]); } private updateBasedOnType(content: string, expectedType: string, bubbleElement: HTMLElement, isOverwrite = false) {