UNPKG

@progress/kendo-ui

Version:

This package is part of the [Kendo UI for jQuery](http://www.telerik.com/kendo-ui) suite.

1,427 lines (1,417 loc) 125 kB
import { a as htmlService, c as Widget, d as utilsService, i as dateUtilsService, n as widgetRegistryService, o as dateParserService, r as fileUtilsService, s as formatterService, u as domUtilsService } from "./kendo.core-Dic9NZAL.js"; import { r as PromptBox } from "./kendo.promptbox-BTu6cTMO.js"; //#region ../src/chat/collaborators/data-manager.collaborator.ts /** * DataManager handles all data operations for the Chat component. * Manages message storage, retrieval, updates, and data source configuration. * * This is an internal collaborator - not a shared service. */ var DataManager = class DataManager { static { this.DEFAULT_OPTIONS = { autoSync: true, schema: { model: { id: "id", fields: { id: { type: "string" }, text: { type: "string" }, authorId: { type: "string" }, authorName: { type: "string" }, authorImageUrl: { type: "string" }, authorImageAltText: { type: "string" }, replyToId: { type: "string", defaultValue: null }, isDeleted: { type: "boolean", defaultValue: false }, isPinned: { type: "boolean", defaultValue: false }, isTyping: { type: "boolean", defaultValue: false }, timestamp: { type: "date" }, files: { parse: (value) => value || [] }, suggestedActions: { parse: (value) => value || [] }, status: { type: "string", defaultValue: null }, failed: { type: "boolean", defaultValue: false }, attachments: { parse: (value) => value || null }, attachmentLayout: { type: "string", defaultValue: null } } } } }; } static { this.FIELD_MAP = { text: "textField", authorId: "authorIdField", authorName: "authorNameField", authorImageUrl: "authorImageUrlField", authorImageAltText: "authorImageAltTextField", id: "idField", timestamp: "timestampField", files: "filesField", suggestedActions: "suggestedActionsField", replyToId: "replyToIdField", isDeleted: "isDeletedField", isPinned: "isPinnedField", isTyping: "isTypingField", status: "statusField", failed: "failedField", attachments: "attachmentsField", attachmentLayout: "attachmentLayoutField" }; } /** * Constructs a new DataManager instance. * @param context - Context with chat options and services */ constructor(context) { this.context = context; this.options = this.buildOptions(context.chatOptions); this.dataSource = this.createDataSource(context.chatOptions); } /** * Builds internal options from chat options. */ buildOptions(chatOptions) { let options; if (chatOptions.dataSource instanceof kendo.data.DataSource) { options = $.extend(true, {}, DataManager.DEFAULT_OPTIONS, chatOptions.dataSource.options); } else if (Array.isArray(chatOptions.dataSource)) { options = $.extend(true, {}, DataManager.DEFAULT_OPTIONS, { data: chatOptions.dataSource }); } else { options = $.extend(true, {}, DataManager.DEFAULT_OPTIONS, chatOptions.dataSource); } options.autoAssignId = chatOptions.autoAssignId; this.mapFields(options.schema.model.fields, chatOptions); return options; } /** * Creates and configures a data source for the chat component. */ createDataSource(chatOptions) { if (chatOptions.dataSource instanceof kendo.data.DataSource) { return chatOptions.dataSource; } return kendo.data.DataSource.create(this.options); } /** * Maps field configuration from chat options to data source field definitions. */ mapFields(fields, chatOptions) { for (const key in fields) { if (Object.prototype.hasOwnProperty.call(fields, key) && DataManager.FIELD_MAP[key]) { const optionKey = DataManager.FIELD_MAP[key]; fields[key].from = chatOptions[optionKey]; } } } /** * Gets the data source instance. */ getDataSource() { return this.dataSource; } /** * Gets the current data source view. */ getView() { return this.dataSource.view(); } /** * Gets the last message in the data source view. */ getLastMessage() { const view = this.getView(); return view.length ? view[view.length - 1] : null; } /** * Gets a message by ID from the data source. */ getMessageById(id) { if (!id) { return null; } return this.dataSource.get(id); } /** * Gets a message by UID from the data source. */ getMessageByUid(uid) { return this.dataSource.getByUid(uid); } /** * Gets a file by UID from a message's files array. */ getFileByUid(message, uid) { if (!message || !uid) { return null; } return message.files.find((file) => file.uid === uid) || null; } /** * Gets the currently pinned message. */ getPinnedMessage() { return this.dataSource.data().find((m) => m.isPinned) || null; } /** * Adds a new message to the data source. */ postMessage(message, currentUserId) { const messageInput = typeof message === "string" ? { text: message } : message; const messageData = { id: this.options.autoAssignId ? utilsService.guid() : undefined, timestamp: new Date(), files: [], ...messageInput, authorId: messageInput.authorId?.toString() || currentUserId }; return this.dataSource.add(messageData); } /** * Updates a message in the data source. */ updateMessage(message, newData) { if (!message) { return null; } let targetMessage = message; if (!(message instanceof kendo.data.ObservableObject)) { targetMessage = this.getMessageById(message.id); } if (!targetMessage) { return null; } for (const key in newData) { if (Object.prototype.hasOwnProperty.call(newData, key)) { targetMessage.set(key, newData[key]); } } return targetMessage; } /** * Marks a message as deleted. */ removeMessage(message) { if (!message) { return false; } message.set("isDeleted", true); return true; } /** * Pins a message. */ pinMessage(message) { if (!message) { return false; } const pinnedMessage = this.getPinnedMessage(); if (pinnedMessage) { pinnedMessage.set("isPinned", false); } message.set("isPinned", true); return message; } /** * Clears the currently pinned message. */ clearPinnedMessage() { const pinnedMessage = this.getPinnedMessage(); if (pinnedMessage) { pinnedMessage.set("isPinned", false); } } }; //#endregion //#region ../src/chat/constants.ts /** * Chat component constants * * This module contains all constants used throughout the Chat widget * and its collaborators. */ /** Namespace for event binding */ const NS = ".kendoChat"; /** Message width display modes */ const MESSAGE_WIDTH_MODE = { STANDARD: "standard", FULL: "full" }; /** Message status values */ const MESSAGE_STATUS = { SENT: "sent", DELIVERED: "delivered", SEEN: "seen", FAILED: "failed" }; /** Files layout modes */ const FILES_LAYOUT_MODE = { HORIZONTAL: "horizontal", VERTICAL: "vertical", WRAP: "wrap" }; /** Suggested actions layout modes */ const SUGGESTED_ACTIONS_LAYOUT_MODE = { SCROLL: "scroll", WRAP: "wrap", SCROLLBUTTONS: "scrollbuttons" }; /** Icon identifiers used in the Chat component */ const ICONS = { attachment: "paperclip-outline-alt-right", attachmentMenu: "more-vertical", checkCircle: "check-circle", chevronDown: "chevron-down", chevronLeft: "chevron-left", chevronRight: "chevron-right", chevronUp: "chevron-up", download: "download", fileError: "file-error", filePdf: "file-pdf", microphoneOutline: "microphone-outline", pin: "pin", undo: "undo", copy: "copy", trash: "trash", send: "paper-plane", stop: "stop-sm", upload: "upload", warningTriangle: "warning-triangle", x: "x", arrowDown: "arrow-down", retry: "arrow-rotate-cw-outline" }; /** Command identifiers for menu actions */ const COMMANDS = { reply: "reply", copy: "copy", pin: "pin", delete: "delete", download: "download" }; /** CSS class names used in the Chat component */ const STYLES = { active: "k-active", selected: "k-selected", focus: "k-focus", expanded: "k-expanded", disabled: "k-disabled", hidden: "k-hidden", generating: "k-generating", noAvatar: "k-no-avatar", file: "k-file-box", fileInfo: "k-file-info", fileName: "k-file-name", fileSize: "k-file-size", fileWrapper: "k-file-box-wrapper", fileWrapperScrollableStart: "k-file-box-wrapper-scrollable-start", filesScroll: "k-files-scroll", filesVertical: "k-files-vertical", filesWrap: "k-files-wrap", avatar: "k-avatar", bubble: "k-bubble", bubbleContent: "k-bubble-content", bubbleExpandable: "k-bubble-expandable", bubbleExpandableIndicator: "k-bubble-expandable-indicator", chatBubble: "k-chat-bubble", chatBubbleText: "k-chat-bubble-text", button: "k-button", buttonDefaults: "k-button-md k-rounded-md k-button-solid k-button-solid-base", buttonIcon: "k-button-icon", buttonPrimary: "k-button-md k-rounded-md k-button-flat k-button-flat-primary", iconButton: "k-icon-button", menuButton: "k-menu-button", chatSend: "k-chat-send", chatUpload: "k-chat-upload", downloadButton: "k-chat-download-button", downloadButtonWrapper: "k-chat-download-button-wrapper", canvas: "k-chat-canvas", card: "k-card", cardAction: "k-card-action", cardActions: "k-card-actions", cardActionsVertical: "k-actions-vertical", cardBody: "k-card-body", cardDeck: "k-card-deck", cardDeckScrollWrap: "k-card-deck-scrollwrap", cardList: "k-card-list", cardMedia: "k-card-media", cardRich: "k-card-type-rich", cardSubtitle: "k-card-subtitle", cardTitle: "k-card-title", cardWrapper: "k-card-container", dropzoneHint: "k-dropzone-hint", dropzoneIcon: "k-dropzone-icon", dropzoneInner: "k-dropzone-inner", externalDropzone: "k-external-dropzone", header: "k-chat-header", message: "k-message", messageAuthor: "k-message-author", messageGroup: "k-message-group", messageGroupContent: "k-message-group-content", messageGroupFullWidth: "k-message-group-full-width", messageGroupReceiver: "k-message-group-receiver", messageGroupSender: "k-message-group-sender", messageListContent: "k-message-list-content", messageListContentEmpty: "k-message-list-content-empty", messageRemoved: "k-message-removed", messageStatus: "k-message-status", messageTime: "k-message-time", messageInfo: "k-message-info", messageFailed: "k-message-failed", messageFailedContent: "k-message-failed-content", messageFailedText: "k-message-failed-text", messageToolbar: "k-chat-message-toolbar", messageRetryButton: "k-resend-button", messagePinned: "k-message-pinned", messageReference: "k-message-reference", messageReferenceContent: "k-message-reference-content", messageReferenceReceiver: "k-message-reference-receiver", messageReferenceSender: "k-message-reference-sender", scrollButtonIconLeft: "chevron-left", scrollButtonIconRight: "chevron-right", suggestion: "k-suggestion", suggestionGroup: "k-suggestion-group", suggestionGroupScrollable: "k-suggestion-group-scrollable", suggestionGroupWrap: "k-suggestion-group-wrap", suggestionsScroll: "k-suggestions-scroll", suggestionScrollWrap: "k-suggestion-scrollwrap", suggestionScrollWrapGradient: "k-suggestion-scrollwrap-gradient", timestamp: "k-timestamp", typingIndicator: "k-typing-indicator", viewWrapper: "k-message-list", wrapper: "k-chat", messageBox: "k-message-box", messageBoxWrapper: "k-message-box-wrapper", messageBoxSeparator: "k-separator", spacer: "k-spacer", scrollFab: "k-chat-scroll-fab", fab: "k-fab" }; /** Data attribute references for element selection */ const REFERENCES = { fileButton: "ref-chat-file-button", fileMenuButton: "ref-chat-file-menu-button", fileWrapper: "ref-chat-file-wrapper", fileCloseButton: "ref-chat-file-close-button", attachmentActionButton: "ref-chat-attachment-action-button", userStatusWrapper: "ref-chat-user-status-wrapper", bubbleExpandableIndicator: "ref-chat-bubble-expandable-indicator", messageReferencePinWrapper: "ref-chat-message-reference-pin-wrapper", messageReferenceReplyWrapper: "ref-chat-message-reference-reply-wrapper", pinnedMessageCloseButton: "ref-chat-pinned-message-close-button", replyMessageCloseButton: "ref-chat-reply-message-close-button", leftScrollButton: "ref-chat-left-scroll-button", rightScrollButton: "ref-chat-right-scroll-button", sendButton: "ref-chat-message-box-send-button", speechToTextButton: "ref-chat-message-box-speech-to-text-button", suggestionGroup: "ref-chat-suggestion-group", fileUploadInput: "ref-chat-file-upload-input", viewWrapper: "ref-chat-view-wrapper", scrollToBottomButton: "ref-chat-scroll-to-bottom-button", retryButton: "ref-chat-message-retry-button" }; /** Event names triggered by the Chat component */ const EVENTS = { sendMessage: "sendMessage", suggestionClick: "suggestionClick", unpin: "unpin", input: "input", toolbarAction: "toolbarAction", fileMenuAction: "fileMenuAction", contextMenuAction: "contextMenuAction", download: "download", fileSelect: "fileSelect", fileRemove: "fileRemove", executeAction: "executeAction", resendMessage: "resendMessage", suggestedActionClick: "suggestedActionClick", messageToolbarExecute: "messageToolbarExecute", fileMenuExecute: "fileMenuExecute", downloadAllFiles: "downloadAllFiles", expandableToggle: "expandableToggle", replyMessageCloseButtonClick: "replyMessageCloseButtonClick" }; /** Common DOM event names */ const CLICK = "click"; const INPUT = "input"; const KEYDOWN = "keydown"; const FOCUS = "focus"; const BLUR = "blur"; const CHANGE = "change"; const SCROLL = "scroll"; /** Text direction constants */ const LTR = "ltr"; const RTL = "rtl"; const K_RTL = "k-rtl"; /** CSS selector prefix */ const DOT = "."; /** Scrolling delta for suggestion navigation */ const SCROLLING_DELTA = 200; /** Scroll to bottom threshold in pixels */ const SCROLL_TO_BOTTOM_THRESHOLD = 100; //#endregion //#region ../src/chat/collaborators/accessibility-manager.collaborator.ts const NAVIGATION_NS = `${NS}navigation`; const ON_FOCUS_MESSAGE_TIME_SELECTOR = `.${STYLES.messageTime}[data-timestamp-visibility='onFocus']`; function toggleOnFocusMessageTime($bubble, visible) { $bubble.closest(`.${STYLES.message}`).find(ON_FOCUS_MESSAGE_TIME_SELECTOR).toggleClass(STYLES.hidden, !visible); } let isKeyEvent = null; let isShiftKey = false; /** * AccesibilityManager handles ARIA attributes and keyboard navigation * for the Chat component according to WCAG 2.1 AA compliance. * * This is an internal collaborator - not a shared service. */ var AccesibilityManager = class { /** * Constructs a new AccesibilityManager instance. * @param context - Context with wrapper, options, and services */ constructor(context) { this.context = context; } /** * Sets up ARIA attributes for the Chat component. */ setupAriaAttributes() { const options = this.context.getOptions(); const messages = options.messages; const messageList = this.context.wrapper.find(`.${STYLES.viewWrapper}`); if (messageList.length) { messageList.attr("role", "log").attr("aria-live", "polite").attr("aria-label", messages.messageListLabel); } this.setupBubbleTabNavigation(); this.setupSuggestionAccessibility(); this.setupButtonAccessibility(messages); this.setupExpandableIndicators(); this.setupScrollButtonsAria(); this.setupMessageBoxAria(messages); } /** * Sets up tabindex navigation for chat bubbles. */ setupBubbleTabNavigation() { const allBubbles = this.context.wrapper.find(`.${STYLES.bubble}`); const interactableBubbles = allBubbles.filter(function() { return $(this).find(`.${STYLES.typingIndicator}`).length === 0; }); const typingIndicatorBubbles = allBubbles.filter(function() { return $(this).find(`.${STYLES.typingIndicator}`).length > 0; }); interactableBubbles.attr("tabindex", "0"); typingIndicatorBubbles.attr("tabindex", "-1"); interactableBubbles.off(`${FOCUS}${NAVIGATION_NS} ${BLUR}${NAVIGATION_NS} ${CLICK}${NAVIGATION_NS}`).on(`${FOCUS}${NAVIGATION_NS}`, function() { const $this = $(this); allBubbles.each(function() { toggleOnFocusMessageTime($(this), false); }); if (isKeyEvent) { allBubbles.removeClass(STYLES.selected); } allBubbles.removeClass(STYLES.focus); if (isKeyEvent) { $this.addClass(STYLES.selected); } $this.addClass(STYLES.focus); toggleOnFocusMessageTime($this, true); isKeyEvent = false; }).on(`${BLUR}${NAVIGATION_NS}`, function() { if (isKeyEvent && !isShiftKey) { $(this).removeClass(STYLES.selected); } toggleOnFocusMessageTime($(this), false); $(this).removeClass(STYLES.focus); }).on(`${CLICK}${NAVIGATION_NS}`, function(e) { if ($(e.target).closest(REFERENCES.fileMenuButton)) { return; } $(this).trigger(FOCUS); }); } /** * Sets up accessibility for suggestion groups. */ setupSuggestionAccessibility() { const suggestionGroups = this.context.wrapper.find(`.${STYLES.suggestionGroup}`); suggestionGroups.each((_index, element) => { const $group = $(element); $group.attr("role", "group"); const suggestions = $group.find(`.${STYLES.suggestion}`); suggestions.each((_suggestionIndex, suggestionElement) => { const $suggestion = $(suggestionElement); $suggestion.attr("role", "button").attr("tabindex", "0"); }); }); } /** * Sets up accessibility for various buttons. */ setupButtonAccessibility(messages) { const suffixButtons = this.context.wrapper.find(".k-input-suffix > .k-button"); suffixButtons.each((_index, element) => { const $button = $(element); if (!$button.attr("role") && element.nodeName.toLowerCase() !== "button") { $button.attr("role", "button"); } if (!$button.attr("aria-label") && !$button.attr("title")) { if ($button.hasClass(STYLES.chatSend)) { $button.attr("aria-label", messages.actionButton || messages.sendButton); } else if ($button.hasClass("k-chat-upload")) { $button.attr("aria-label", messages.fileButton); } } if ($button.hasClass(STYLES.chatSend) && $button.hasClass("k-disabled")) { $button.attr("aria-disabled", "true"); } }); const downloadButtons = this.context.wrapper.find(`.${STYLES.downloadButton}`); downloadButtons.each((_index, element) => { const $button = $(element); if (!$button.attr("role") && element.nodeName.toLowerCase() !== "button") { $button.attr("role", "button"); } if (!$button.attr("aria-label") && !$button.attr("title")) { $button.attr("aria-label", messages.downloadAll); } }); this.setupCloseButtonAria(REFERENCES.pinnedMessageCloseButton, messages.pinnedMessageCloseButton); this.setupCloseButtonAria(REFERENCES.replyMessageCloseButton, messages.replyMessageCloseButton); const fileMenuButtons = this.context.wrapper.find(`[${REFERENCES.fileMenuButton}]`); fileMenuButtons.each((_index, element) => { const $button = $(element); $button.attr("aria-label", messages.fileMenuButton); $button.attr("title", messages.fileMenuButton); }); } /** * Sets up ARIA attributes for close buttons. */ setupCloseButtonAria(reference, label) { const buttons = this.context.wrapper.find(`[${reference}]`); buttons.each((_index, element) => { const $button = $(element); if (!$button.attr("role") && element.nodeName.toLowerCase() !== "button") { $button.attr("role", "button"); } if (!$button.attr("aria-label") && !$button.attr("title")) { $button.attr("aria-label", label); $button.attr("title", label); } }); } /** * Sets up ARIA attributes for expandable indicators. */ setupExpandableIndicators() { const expandableIndicators = this.context.wrapper.find(`.${STYLES.bubbleExpandableIndicator}`); expandableIndicators.each((_index, element) => { const $indicator = $(element); $indicator.attr("role", "button").attr("tabindex", "0"); const bubble = $indicator.closest(`.${STYLES.bubble}`); const isExpanded = bubble.hasClass(STYLES.expanded); const label = isExpanded ? "Collapse message" : "Expand message"; $indicator.attr("aria-label", label); }); } /** * Sets up ARIA attributes for scroll buttons. */ setupScrollButtonsAria() { const dir = this.context.getOptions().dir; const leftScrollButton = this.context.wrapper.find(`[${REFERENCES.leftScrollButton}]`); const rightScrollButton = this.context.wrapper.find(`[${REFERENCES.rightScrollButton}]`); const leftText = dir === "rtl" ? "Scroll Right" : "Scroll Left"; const rightText = dir === "rtl" ? "Scroll Left" : "Scroll Right"; leftScrollButton.attr("aria-label", leftText); leftScrollButton.attr("title", leftText); rightScrollButton.attr("aria-label", rightText); rightScrollButton.attr("title", rightText); } /** * Sets up ARIA attributes for the message box. */ setupMessageBoxAria(messages) { const messageBox = this.context.wrapper.find(`.${STYLES.messageBox}`); messageBox.find("textarea, input:not([type='hidden']):not([type='file']):not([type='button']):not([type='submit']):not([type='reset']):not([type='checkbox']):not([type='radio'])").first().attr("aria-label", messages.messageBoxLabel); } /** * Sets up ARIA attributes for file close buttons. */ setFileCloseButtonAria(container) { const fileCloseButton = container.find(`[${REFERENCES.fileCloseButton}]`); fileCloseButton.each((_index, element) => { const $button = $(element); if (!$button.attr("role") && element.nodeName.toLowerCase() !== "button") { $button.attr("role", "button"); } if (!$button.attr("aria-label") && !$button.attr("title")) { $button.attr("aria-label", "Remove selected file"); $button.attr("title", "Remove selected file"); } }); } /** * Updates ARIA attributes for expandable indicators when their state changes. */ updateExpandableIndicatorAria(indicator, isExpanded) { const label = isExpanded ? "Collapse message" : "Expand message"; indicator.attr("aria-label", label); } /** * Handles keyboard navigation for the Chat component. */ handleKeyDown(e, messageBox) { if (!$(e.target).closest(this.context.wrapper).length) { return null; } const target = $(e.target); const key = e.keyCode || e.which; isKeyEvent = true; isShiftKey = e.shiftKey; if (target.hasClass(STYLES.bubble)) { this.handleBubbleKeyDown(e, key); } if (target.hasClass(STYLES.suggestion)) { this.handleClickableElementKeyDown(e, key); } if (target.hasClass(STYLES.bubbleExpandableIndicator)) { this.handleExpandableIndicatorKeyDown(e, key, target); } if (target.hasClass("k-input-inner")) { this.handleMessageInputKeyDown(e, key, messageBox); } if (target.closest(".k-input-suffix .k-button").length) { this.handleClickableElementKeyDown(e, key); } return null; } /** * Handles keyboard navigation within bubbles. */ handleBubbleKeyDown(e, key) { const currentBubble = $(e.target); if (currentBubble.find(`.${STYLES.typingIndicator}`).length > 0) { return; } const allBubbles = this.context.wrapper.find(`.${STYLES.bubble}`); const bubbles = allBubbles.filter(function() { return $(this).find(`.${STYLES.typingIndicator}`).length === 0; }); const currentIndex = bubbles.index(currentBubble); const keys = utilsService.keys; switch (key) { case keys.UP: e.preventDefault(); this.focusBubbleAtIndex(bubbles, currentIndex - 1); break; case keys.DOWN: e.preventDefault(); this.focusBubbleAtIndex(bubbles, currentIndex + 1); break; case keys.HOME: e.preventDefault(); this.focusBubbleAtIndex(bubbles, 0); break; case keys.END: e.preventDefault(); this.focusBubbleAtIndex(bubbles, bubbles.length - 1); break; } } /** * Focuses a bubble at the specified index. */ focusBubbleAtIndex(bubbles, index) { if (bubbles.length === 0 || index < 0 || index >= bubbles.length) { return; } const targetBubble = bubbles.eq(index); targetBubble.attr("data-keyboard-focus", "true"); targetBubble.focus(); targetBubble.removeAttr("data-keyboard-focus"); } /** * Handles keyboard events for expandable indicators. */ handleExpandableIndicatorKeyDown(e, key, target) { const keys = utilsService.keys; if (key === keys.ENTER || key === keys.SPACEBAR) { e.preventDefault(); target.trigger(CLICK); const bubble = target.closest(`.${STYLES.bubble}`); const isExpanded = bubble.hasClass(STYLES.expanded); this.updateExpandableIndicatorAria(target, !isExpanded); } } /** * Handles keyboard events for the message input. * Note: Enter key handling is delegated to PromptBox component. */ handleMessageInputKeyDown(e, key, messageBox) {} /** * Handles keyboard events for clickable elements (suggestions, buttons). */ handleClickableElementKeyDown(e, key) { const keys = utilsService.keys; if (key === keys.ENTER || key === keys.SPACEBAR) { e.preventDefault(); $(e.target).trigger(CLICK); } } }; //#endregion //#region ../src/chat/collaborators/message-toolbar.collaborator.ts /** * MessageToolbar provides toolbar functionality for chat messages. * Handles quick actions that can be performed on messages through toolbar buttons. * * This is an internal collaborator that extends Widget for event handling. */ var MessageToolbar = class MessageToolbar extends Widget { static { this.options = { name: "MessageToolbar" }; } /** * Constructs a new MessageToolbar instance. */ constructor(element, options) { super(element, $.extend(true, {}, MessageToolbar.options, options)); this.extendItemsConfig(); this.create(); this.attachEvents(); } /** * Creates the underlying Kendo UI ToolBar. */ create() { const existingToolBar = this.element.data("kendoToolBar"); if (existingToolBar) { existingToolBar.destroy(); } const toolbarOptions = $.extend({}, this.options, { fillMode: "flat" }); delete toolbarOptions.name; this.toolbar = new kendo.ui.ToolBar(this.element, toolbarOptions); } /** * Extends items configuration with default properties. */ extendItemsConfig() { const items = this.options.items; if (items) { items.forEach((item) => { item.attributes = item.attributes || {}; const ariaLabel = item.attributes["aria-label"]; item.attributes["data-command"] = item.name.toLowerCase(); item.attributes["aria-label"] = ariaLabel ?? item.name; item.type = "button"; item.fillMode = "flat"; item.overflow = "never"; }); } } /** * Attaches event handlers to the toolbar. */ attachEvents() { this.toolbar.bind(CLICK, this.onClick.bind(this)); } /** * Handles toolbar button clicks. */ onClick(e) { const message = e.target.closest(DOT + STYLES.message); const command = e.target.data("command"); if (command) { this.executeCommand(command, e.target, message); } } /** * Executes a command from the toolbar. */ executeCommand(command, item, message) { this.trigger("execute", { type: command, item, message }); } destroy() { if (this.toolbar) { this.toolbar.destroy(); this.toolbar = null; } this.element.empty(); } }; //#endregion //#region ../src/chat/collaborators/file-menu.collaborator.ts /** * FileMenu provides dropdown button functionality for file attachments. * Handles actions like download, preview, and delete for individual files. * * This is an internal collaborator that extends Widget for event handling. */ var FileMenu = class FileMenu extends Widget { static { this.options = { name: "ChatFileMenu", items: [] }; } /** * Constructs a new FileMenu instance. */ constructor(element, options) { super(element, $.extend(true, {}, FileMenu.options, options)); this.setCommandAttributes(); this.createDropdownButton(); } /** * Creates a dropdown button on the element. */ createDropdownButton() { this.dropdownButton = new kendo.ui.DropDownButton(this.element, { items: this.options.items, fillMode: "flat", icon: ICONS.attachmentMenu }); this.dropdownButton.bind("click", this.onClick.bind(this)).bind("open", this.onOpen.bind(this)).bind("close", this.onClose.bind(this)); } /** * Sets command attributes on menu items for identification. */ setCommandAttributes() { this.options.items.forEach((item) => { item.attributes = item.attributes || {}; item.attributes["data-command"] = item.name.toLowerCase(); item.id = item.name.toLowerCase(); }); } /** * Handles dropdown button item click. */ onClick(e) { const command = e.id; const file = $(e.sender.element).closest(DOT + STYLES.file); const message = $(e.sender.element).closest(DOT + STYLES.message); if (command) { this.executeCommand(command, $(e.sender.element), file, message); } } /** * Handles dropdown open event. */ onOpen(e) { let setActive = true; const target = $(e.sender.element); if (target.closest(DOT + STYLES.messageRemoved).length || target.find(DOT + STYLES.typingIndicator).length) { e.preventDefault(); setActive = false; } this.setActive(target, setActive); } /** * Handles dropdown close event. */ onClose(e) { const target = $(e.sender.element); const message = target.closest(DOT + STYLES.message); this.setActive(target, false); this.trigger("close", { message }); } /** * Executes a command from the file menu. */ executeCommand(command, item, file, message) { this.trigger("execute", { type: command, item, file, message }); } /** * Sets the active state for a target element. */ setActive(target, active) { const bubble = target.closest(DOT + STYLES.bubble); if (active) { bubble.addClass(STYLES.active); } else { bubble.removeClass(STYLES.active); } } destroy() { if (this.dropdownButton) { this.dropdownButton.destroy(); this.dropdownButton = null; } super.destroy(); } }; //#endregion //#region ../src/chat/templates/common.template.ts /** * Renders an avatar element for a message author. */ const renderAvatar = (url, altText) => { return new kendo.ui.Avatar("<div>", { type: "image", image: url, alt: htmlService.encode(altText ?? "") }).wrapper[0].outerHTML; }; /** * Renders a typing indicator animation. */ const renderTypingIndicator = () => { return `<div class="${STYLES.typingIndicator}"> <span></span> <span></span> <span></span> </div>`; }; /** * Renders a single file attachment item. * Uses small-sized buttons for menu/download actions per Chat v3 spec. */ const renderFile = (file, closeButton, fileMenuButton = true) => { const closeButtonHtml = closeButton ? new kendo.ui.Button(`<button class="" ${REFERENCES.fileCloseButton}>`, { icon: ICONS.x, fillMode: "flat", size: "small" }).wrapper[0].outerHTML : ""; const fileMenuButtonHtml = fileMenuButton && !closeButton ? new kendo.ui.Button(`<button class="${STYLES.menuButton}" ${REFERENCES.fileMenuButton}>`, { icon: ICONS.attachmentMenu, fillMode: "flat", size: "small" }).wrapper[0].outerHTML : ""; return `<li class="${STYLES.file}" data-uid="${htmlService.encode(file.uid)}"> ${kendo.ui.icon({ icon: fileUtilsService.getFileGroup(file.extension, true), size: "xlarge" })} <div class="${STYLES.fileInfo}"> <span class="${STYLES.fileName}">${htmlService.encode(file.name)}</span> <span class="${STYLES.fileSize}">${htmlService.encode(fileUtilsService.getFileSizeMessage(file.size))}</span> </div> ${closeButtonHtml} ${fileMenuButtonHtml} </li>`; }; //#endregion //#region ../src/chat/templates/file.template.ts /** * Renders a list of file attachments with layout mode support. * Uses small-sized buttons per Chat v3 spec. */ const renderFiles = (files, downloadAll, messages, closeButton, layoutMode = FILES_LAYOUT_MODE.VERTICAL) => { if (!files?.length) { return ""; } let fileItems = ""; files.forEach((file) => { fileItems += renderFile(file, closeButton ?? false, true); }); let layoutClass = ""; if (layoutMode === FILES_LAYOUT_MODE.VERTICAL) { layoutClass = ` ${STYLES.filesVertical}`; } else if (layoutMode === FILES_LAYOUT_MODE.WRAP) { layoutClass = ` ${STYLES.filesWrap}`; } let html = `<ul class="${STYLES.fileWrapper}${layoutClass}" ${REFERENCES.fileWrapper}>`; if (layoutMode === FILES_LAYOUT_MODE.HORIZONTAL) { html += `<div class="${STYLES.filesScroll}">${fileItems}</div>`; } else { html += fileItems; } html += "</ul>"; if (downloadAll && files.length > 1 && messages?.downloadAll) { html += `<div class="${STYLES.downloadButtonWrapper}"> ${new kendo.ui.Button(`<button class="${STYLES.downloadButton}">${messages.downloadAll}</button>`, { icon: ICONS.download, fillMode: "flat", size: "small" }).wrapper[0].outerHTML} </div>`; } return html; }; /** * Renders a list of file attachments with the specified layout mode. * Uses small-sized buttons per Chat v3 spec. * @param files - Array of files to render * @param downloadAll - Whether to show a "download all" button * @param messages - Localization messages * @param closeButton - Whether to show close buttons on files * @param layoutMode - Layout mode: "horizontal" | "vertical" | "wrap" */ const renderFilesWithLayout = (files, downloadAll, messages, closeButton, layoutMode = FILES_LAYOUT_MODE.HORIZONTAL) => { if (!files?.length) { return ""; } let fileItems = ""; files.forEach((file) => { fileItems += renderFile(file, closeButton ?? false, true); }); let layoutClass = ""; if (layoutMode === FILES_LAYOUT_MODE.VERTICAL) { layoutClass = ` ${STYLES.filesVertical}`; } else if (layoutMode === FILES_LAYOUT_MODE.WRAP) { layoutClass = ` ${STYLES.filesWrap}`; } let html = `<ul class="${STYLES.fileWrapper}${layoutClass}" ${REFERENCES.fileWrapper}>`; if (layoutMode === FILES_LAYOUT_MODE.HORIZONTAL) { html += `<div class="${STYLES.filesScroll}">${fileItems}</div>`; } else { html += fileItems; } html += "</ul>"; if (downloadAll && files.length > 1) { html += `<div class="${STYLES.downloadButtonWrapper}"> ${new kendo.ui.Button(`<button class="${STYLES.downloadButton}">${messages.downloadAll}</button>`, { icon: ICONS.download, fillMode: "flat", size: "small" }).wrapper[0].outerHTML} </div>`; } return html; }; /** * Renders a message reference (for reply or pinned messages). */ const renderMessageReference = (context) => { const { text, files, isOwnMessage, isPinMessage, isDeleted, renderCloseButton, renderFileMenuButton, messages } = context; const messageReferenceSenderStyle = isOwnMessage ? STYLES.messageReferenceSender : STYLES.messageReferenceReceiver; const pinMessageReferenceStyle = isPinMessage ? STYLES.messagePinned : ""; const closeButtonReference = isPinMessage ? REFERENCES.pinnedMessageCloseButton : REFERENCES.replyMessageCloseButton; const wrapperReference = isPinMessage ? REFERENCES.messageReferencePinWrapper : REFERENCES.messageReferenceReplyWrapper; let content = htmlService.convertTextUrlToLink(text || ""); if (!content) { content = files?.length ? renderFile(files[0], false, renderFileMenuButton ?? false) : ""; } if (isDeleted && messages) { content = isOwnMessage ? htmlService.encode(messages.selfMessageDeleted) : htmlService.encode(messages.otherMessageDeleted); } return `<div class="${STYLES.messageReference} ${messageReferenceSenderStyle} ${pinMessageReferenceStyle}" ${wrapperReference}> ${isPinMessage ? kendo.ui.icon({ icon: ICONS.pin, size: "xlarge" }) : ""} <div class="${STYLES.messageReferenceContent}">${content}</div> <span class="${STYLES.spacer}"></span> ${renderCloseButton ? new kendo.ui.Button(`<button ${closeButtonReference}>`, { icon: ICONS.x, fillMode: "flat" }).wrapper[0].outerHTML : ""} </div>`; }; //#endregion //#region ../src/chat/templates/message.template.ts /** * Renders the retry button for failed messages. */ const renderRetryButton = (message, retryText) => { if (!message.failed || !message.isOwnMessage) { return ""; } return kendo.html.renderButton(`<button class="${STYLES.messageRetryButton}" ${REFERENCES.retryButton} title="${retryText}" aria-label="${retryText}" data-uid="${message.uid || ""}"></button>`, { icon: ICONS.retry, size: "small", fillMode: "clear" }); }; const renderFailedContent = (failedText) => { return `<span class="${STYLES.messageFailedContent}"> ${kendo.ui.icon({ icon: ICONS.warningTriangle, size: "xsmall" })} <span class="${STYLES.messageFailedText}">${htmlService.encode(failedText)}</span> </span>`; }; /** * Renders the message status indicator. * Supports custom status settings with icon, text, and cssClass for enhanced display. */ const renderMessageStatus = (status, message, statusTemplate, statusSettings) => { if (!status) { return ""; } if (statusTemplate) { return statusTemplate({ status, message }); } const settings = statusSettings?.[status]; const statusClass = `${STYLES.messageStatus}${settings?.cssClass ? " " + settings.cssClass : ""}`; let iconHtml = ""; if (settings?.icon) { iconHtml = kendo.ui.icon({ icon: settings.icon }); } const statusText = settings?.text !== undefined ? settings.text : status.charAt(0).toUpperCase() + status.slice(1); return `<span class="${statusClass}">${iconHtml}${htmlService.encode(statusText)}</span>`; }; /** * Renders a single attachment card. */ const renderAttachment = (attachment, message, attachmentIndex, attachmentTemplate) => { if (attachmentTemplate) { return attachmentTemplate({ attachment, message }); } const title = attachment.title ? `<div class="${STYLES.cardTitle}">${htmlService.encode(attachment.title)}</div>` : ""; const subtitle = attachment.subtitle ? `<div class="${STYLES.cardSubtitle}">${htmlService.encode(attachment.subtitle)}</div>` : ""; const image = attachment.thumbnailUrl ? `<div class="${STYLES.cardMedia}"><img src="${htmlService.encode(attachment.thumbnailUrl)}" alt="" /></div>` : ""; const actions = attachment.actions?.length ? `<div class="${STYLES.cardActions}">${attachment.actions.map((action, actionIndex) => { const label = action.title || action.text; return `<button class="${STYLES.cardAction} ${STYLES.button}" ${REFERENCES.attachmentActionButton} data-attachment-index="${attachmentIndex}" data-action-index="${actionIndex}">${htmlService.encode(label)}</button>`; }).join("")}</div>` : ""; return `<div class="${STYLES.card}"> ${image} <div class="${STYLES.cardBody}"> ${title} ${subtitle} </div> ${actions} </div>`; }; /** * Renders attachments with the specified layout. */ const renderAttachments = (attachments, message, layout = "list", attachmentTemplate) => { if (!attachments?.length) { return ""; } const attachmentsHtml = attachments.map((attachment, attachmentIndex) => renderAttachment(attachment, message, attachmentIndex, attachmentTemplate)).join(""); if (layout === "carousel") { return `<div class="${STYLES.cardDeckScrollWrap}"> ${new kendo.ui.Button(`<button ${REFERENCES.leftScrollButton}>`, { icon: ICONS.chevronLeft }).wrapper[0].outerHTML} <div class="${STYLES.cardDeck}">${attachmentsHtml}</div> ${new kendo.ui.Button(`<button ${REFERENCES.rightScrollButton}>`, { icon: ICONS.chevronRight }).wrapper[0].outerHTML} </div>`; } return `<div class="${STYLES.cardList}">${attachmentsHtml}</div>`; }; /** * Renders a single chat message. */ const renderMessage = (message, replyMessage, downloadAll, messages, expandable, messageTimeFormat, skipSanitization, statusTemplate, filesLayoutMode = FILES_LAYOUT_MODE.VERTICAL, contentTemplate, attachmentTemplate, attachmentLayout, timestampVisibility = "onFocus", statusSettings, showMessageTime = true) => { const isDeleted = message.isDeleted; const isFailedStatus = message.status === MESSAGE_STATUS.FAILED; const isFailed = message.isOwnMessage && (message.failed || isFailedStatus); const messageStatus = !message.isOwnMessage && isFailedStatus ? undefined : message.status; const expandableClasses = expandable && !message.isTyping ? [STYLES.bubbleExpandable, STYLES.expanded] : []; const replyMessageHtml = replyMessage ? renderMessageReference({ text: replyMessage.text, files: replyMessage.files, isOwnMessage: replyMessage.isOwnMessage, isPinMessage: false, renderCloseButton: false, renderFileMenuButton: false }) : ""; let messageContent = ""; if (message.isTyping && !message.isOwnMessage) { messageContent = renderTypingIndicator(); } else if (contentTemplate && !isDeleted) { messageContent = contentTemplate(message); } else if (isDeleted) { messageContent = message.isOwnMessage ? htmlService.encode(messages.selfMessageDeleted) : htmlService.encode(messages.otherMessageDeleted); } else { messageContent = htmlService.convertTextUrlToLink(message.text || "", skipSanitization); } const timeAttributes = timestampVisibility === "onFocus" ? ` data-timestamp-visibility="${timestampVisibility}"` : ""; const timeClasses = [STYLES.messageTime, timestampVisibility === "onFocus" ? STYLES.hidden : ""].filter(Boolean).join(" "); const timeHtml = showMessageTime ? `<time class="${timeClasses}"${timeAttributes}>${formatterService.toString(dateParserService.parseDate(message.timestamp), messageTimeFormat)}</time>` : ""; const statusHtml = renderMessageStatus(messageStatus, message, statusTemplate, statusSettings || null); const retryHtml = isFailed ? renderRetryButton(message, messages.retryMessage || "Retry") : ""; const failedContentHtml = isFailed ? renderFailedContent(statusSettings?.[MESSAGE_STATUS.FAILED]?.text || "Failed to send") : ""; const messageInfoHtml = `<div class="${STYLES.messageInfo}">${statusHtml}${timeHtml}${failedContentHtml}</div>`; return `<div class="${STYLES.message}${isDeleted ? " " + STYLES.messageRemoved : ""}${isFailed ? " " + STYLES.messageFailed : ""}" data-uid="${htmlService.encode(message.uid ?? "")}"> ${retryHtml} <div class="${STYLES.chatBubble} ${STYLES.bubble} ${expandableClasses.join(" ")}"> <div class="${STYLES.bubbleContent}"> ${replyMessageHtml} <span class="${STYLES.chatBubbleText}">${messageContent}</span> ${!isDeleted ? renderFiles(message.files, downloadAll, messages, false, filesLayoutMode) : ""} </div> ${expandable && !message.isTyping ? `<span class="${STYLES.bubbleExpandableIndicator}" ${REFERENCES.bubbleExpandableIndicator}>${kendo.ui.icon({ icon: ICONS.chevronUp })}</span>` : ""} </div> ${messageInfoHtml} ${!isDeleted ? `<div class="${STYLES.messageToolbar}"></div>` : ""} </div>`; }; /** * Renders a message group with author avatar and multiple messages. */ const renderMessageGroup = (context) => { const { message, author, isOwnMessage, replyMessage, downloadAll = true, messages, expandable = false, fullWidth = false, messageTimeFormat = "ddd MMM dd yyyy", timestampTemplate, statusTemplate, showTimestamp = false, messageTemplate, skipSanitization = false, messageSettings, filesLayoutMode = FILES_LAYOUT_MODE.VERTICAL, contentTemplate, attachmentTemplate, attachmentLayout, userStatusTemplate, timestampVisibility = "onFocus", statusSettings, showMessageTime = true } = context; const effectiveShowAvatar = messageSettings?.showAvatar !== undefined ? messageSettings.showAvatar && author && author.imageUrl : author && author.imageUrl; const effectiveShowUsername = messageSettings?.showUsername !== undefined ? messageSettings.showUsername : true; const effectiveExpandable = messageSettings?.allowMessageCollapse !== undefined ? messageSettings.allowMessageCollapse : expandable; const effectiveFullWidth = messageSettings?.messageWidthMode === "full" || fullWidth; const groupClasses = [ STYLES.messageGroup, isOwnMessage ? STYLES.messageGroupSender : STYLES.messageGroupReceiver, effectiveShowAvatar ? "" : STYLES.noAvatar, effectiveFullWidth ? STYLES.messageGroupFullWidth : "" ].filter(Boolean).join(" "); let timestampContent = ""; if (showTimestamp && message.timestamp) { const messageDate = dateParserService.parseDate(message.timestamp); if (utilsService.isFunction(timestampTemplate)) { timestampContent = timestampTemplate({ date: messageDate, message }); } else { const relativeDateText = dateUtilsService.getRelativeDateString(messageDate); timestampContent = `<div class="${STYLES.timestamp}">${relativeDateText}</div>`; } } const userStatusHtml = effectiveShowAvatar && userStatusTemplate ? userStatusTemplate({ message: { ...message, author } }) : ""; const effectiveAttachmentLayout = message.attachmentLayout || attachmentLayout || "list"; const attachmentsHtml = !message.isDeleted ? renderAttachments(message.attachments, message, effectiveAttachmentLayout, attachmentTemplate) : ""; let messageHtml; if (messageTemplate) { messageHtml = messageTemplate({ ...message, isOwnMessage, author }, replyMessage ?? null, downloadAll, messages, effectiveExpandable, messageTimeFormat, skipSanitization, statusTemplate); } else { messageHtml = renderMessage({ ...message, isOwnMessage, author }, replyMessage ?? null, downloadAll, messages, effectiveExpandable, messageTimeFormat, skipSanitization, statusTemplate, filesLayoutMode, contentTemplate, attachmentTemplate, attachmentLayout, timestampVisibility, statusSettings, showMessageTime); } return `${showTimestamp && timestampContent ? timestampContent : ""} <div class="${groupClasses}">${userStatusHtml ? `<div ${REFERENCES.userStatusWrapper}>${userStatusHtml}</div>` : ""} ${effectiveShowAvatar ? renderAvatar(author.imageUrl, author.imageAltText) : ""} <div class="${STYLES.messageGroupContent}"> ${effectiveShowUsername ? `<span class="${STYLES.messageAuthor}">${htmlService.encode(author.name ?? "")}</span>` : ""} ${messageHtml} </div> </div> ${attachmentsHtml}`; }; //#endregion //#region ../src/chat/templates/suggestions.template.ts /** * Renders a list of suggestions/suggested actions with the specified layout mode. * @param suggestions - Array of suggestions to render * @param layoutMode - Layout mode: "scroll" | "wrap" | "scrollbuttons" * @param isRtl - Whether RTL direction is used */ const renderSuggestions = (suggestions) => { if (!suggestions?.length) { return ""; } const suggestionItems = suggestions.map((suggestion) => `<span class="${STYLES.suggestion}">${htmlService.encode(suggestion.text)}</span>`).join(""); return `<div class="${STYLES.suggestionGroup}" ${REFERENCES.suggestionGroup}>${suggestionItems}</div>`; }; /** * Renders suggestions with the specified layout mode from an array of suggestions. * @param suggestions - Array of suggestions to render * @param layoutMode - Layout mode: "scroll" | "wrap" | "scrollbuttons" * @param isRtl - Whether RTL direction is used */ const renderSuggestionsWithLayout = (suggestions, layoutMode = SUGGESTED_ACTIONS_LAYOUT_MODE.SCROLL, isRtl = false) => { if (!suggestions?.length) { return ""; } const suggestionItems = suggestions.map((suggestion) => `<span class="${STYLES.suggestion}">${htmlService.encode(suggestion.text)}</span>`).join(""); return wrapSuggestionsWithLayout(suggestionItems, layoutMode, isRtl); }; /** * Wraps pre-rendered suggestion content with the specified layout mode. * Use this when you have custom-rendered content from a template. * @param content - Pre-rendered HTML content (suggestion items) * @param layoutMode - Layout mode: "scroll" | "wrap" | "scrollbuttons" * @param isRtl - Whether RTL direction is used */ const wrapSuggestionsWithLayout = (content, layoutMode = SUGGESTED_ACTIONS_LAYOUT_MODE.SCROLL, isRtl = false) => { if (!content) { return ""; } if (layoutMode === SUGGESTED_ACTIONS_LAYOUT_MODE.WRAP) { return `<div class="${STYLES.suggestionGroup}" ${REFERENCES.suggestionGroup}>${content}</div>`; } const layoutClass = layoutMode === SUGGESTED_ACTIONS_LAYOUT_MODE.SCROLL ? STYLES.suggestionGroupScrollable : ""; const groupClass = layoutClass ? `${STYLES.suggestionGroup} ${layoutClass}` : STYLES.suggestionGroup; const scrollContent = `<div class="${STYLES.suggestionsScroll}">${content}</div>`; const suggestionsElement = `<div class="${groupClass}" ${REFERENCES.suggestionGroup}>${scrollContent}</div>`; if (layoutMode === SUGGESTED_ACTIONS_LAYOUT_MODE.SCROLLBUTTONS) { return renderScrollableSuggestions(suggestionsElement, isRtl); } return suggestionsElement; }; /** * Renders scrollable suggestions with navigation buttons and gradient styling. */ const renderScrollableSuggestions = (suggestionsElement, isRtl) => { const leftIcon = isRtl ? ICONS.chevronRight : ICONS.chevronLeft; const rightIcon = isRtl ? ICONS.chevronLeft : ICONS.chevronRight; return `<div class="${STYLES.suggestionScrollWrap} ${STYLES.suggestionScrollWrapGradient}"> ${new kendo.ui.Button(`<button ${REFERENCES.leftScrollButton}>`, { icon: leftIcon }).wrapper[0].outerHTML} ${suggestionsElement} ${new kendo.ui.Button(`<button ${REFERENCES.rightScrollButton}>`, { icon: rightIcon }).wrapper[0].outerHTML} </div>`; }; /** * Renders the chat header using Toolbar. * @param items - Array of toolbar items configuration * @param headerTemplate - Optional custom header template that overrides items */ const renderHeader = (items, headerTemplate) => { if (headerTemplate) { return `<div class="${STYLES.header}">${headerTemplate()}</div>`; } if (!items || items.length === 0) { return ""; } const toolbarElement = $(`<div class="${STYLES.header}">`); new kendo.ui.ToolBar(toolbarElement, { items }); return toolbarElement[0].outerHTML; }; //#endregion //#region ../src/chat/collaborators/chat-view/message-renderer.ts var MessageRenderer = class { constructor(context) { this.context = context; } renderMessage(message) { const options = this.context.options; const componentMessages = options.messages; const author = { id: message.authorId, name: message.authorName, imageUrl: me