diff --git a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-fileview/_index.scss b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-fileview/_index.scss index f0befbed030f..dab10e4c6c92 100644 --- a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-fileview/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-fileview/_index.scss @@ -39,7 +39,5 @@ } .dx-chat-file-view { - display: flex; - flex-wrap: wrap; - width: 100%; + display: grid; } diff --git a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-fileview/_mixins.scss b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-fileview/_mixins.scss index 349c4a98bed9..71ae8d4df5b5 100644 --- a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-fileview/_mixins.scss +++ b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-fileview/_mixins.scss @@ -49,8 +49,10 @@ @mixin chat-fileview( $chat-file-view-items-gap, + $chat-file-container-width, ) { .dx-chat-file-view { gap: $chat-file-view-items-gap; + grid-template-columns: repeat(auto-fit, $chat-file-container-width); } } diff --git a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagebubble/_index.scss b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagebubble/_index.scss index c4db59f8da99..6f6115f8efc0 100644 --- a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagebubble/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagebubble/_index.scss @@ -9,6 +9,8 @@ $chat-messagebubble-max-height-image: 400px !default; } .dx-chat-messagebubble { + display: flex; + flex-direction: column; width: auto; word-break: break-word; white-space: break-spaces; diff --git a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagebubble/_mixins.scss b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagebubble/_mixins.scss index 43b4d51abdfe..247cb72c3e61 100644 --- a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagebubble/_mixins.scss +++ b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagebubble/_mixins.scss @@ -8,6 +8,7 @@ $bubble-container-gap, $bubble-delete-gap, $bubble-delete-icon-size, + $chat-bubble-gap, ) { .dx-chat-messagegroup-content { row-gap: $bubble-container-gap; @@ -16,6 +17,7 @@ .dx-chat-messagebubble { padding: $padding; border-radius: $border-radius; + gap: $chat-bubble-gap; &.dx-chat-messagebubble-deleted { .dx-chat-messagebubble-content { diff --git a/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss index 749b370d5f8b..922af1025ab5 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss @@ -29,7 +29,8 @@ $chat-file-file-size-max-width, ); @include chat-fileview( - $chat-file-view-items-gap + $chat-file-view-items-gap, + $chat-file-container-width, ); @include chat-alertlist( $chat-alertlist-color, @@ -56,6 +57,7 @@ $chat-bubble-container-gap, $chat-bubble-delete-gap, $chat-bubble-delete-icon-size, + $chat-bubble-gap, ); @include chat-messagegroup( $chat-messagegroup-gap, diff --git a/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss index 5e92a27e4bfa..9d2868a5583a 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss @@ -74,6 +74,7 @@ $chat-file-secondary-font-size: 10px !default; $chat-file-base-section-gap: 4px !default; $chat-file-file-size-max-width: 4em !default; $chat-file-view-items-gap: 8px !default; +$chat-bubble-gap: 8px !default; @if $size == "default" { $chat-bubble-border-radius: $fluent-base-border-radius * 3 !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss b/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss index 66bc2f90a9e4..9cb8276009cc 100644 --- a/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss @@ -32,7 +32,8 @@ $chat-file-file-size-max-width, ); @include chat-fileview( - $chat-file-view-items-gap + $chat-file-view-items-gap, + $chat-file-container-width, ); @include chat-alertlist( $chat-alertlist-color, @@ -59,6 +60,7 @@ $chat-bubble-container-gap, $chat-bubble-delete-gap, $chat-bubble-delete-icon-size, + $chat-bubble-gap, ); @include chat-messagegroup( $chat-messagegroup-gap, diff --git a/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss index 0e9b87040363..cb25962b4634 100644 --- a/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss @@ -73,6 +73,7 @@ $chat-file-secondary-font-size: 10px !default; $chat-file-base-section-gap: 4px !default; $chat-file-file-size-max-width: 4em !default; $chat-file-view-items-gap: 8px !default; +$chat-bubble-gap: 8px !default; @if $size == "default" { $chat-bubble-padding: 8px 12px !default; diff --git a/packages/devextreme-scss/scss/widgets/material/chat/_index.scss b/packages/devextreme-scss/scss/widgets/material/chat/_index.scss index d72c0a9c1e3d..ab6d3169655f 100644 --- a/packages/devextreme-scss/scss/widgets/material/chat/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/chat/_index.scss @@ -30,7 +30,8 @@ $chat-file-file-size-max-width, ); @include chat-fileview( - $chat-file-view-items-gap + $chat-file-view-items-gap, + $chat-file-container-width, ); @include chat-alertlist( $chat-alertlist-color, @@ -57,6 +58,7 @@ $chat-bubble-container-gap, $chat-bubble-delete-gap, $chat-bubble-delete-icon-size, + $chat-bubble-gap, ); @include chat-messagegroup( $chat-messagegroup-gap, diff --git a/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss index ef2ea1ce0e80..57b06038b9ba 100644 --- a/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss @@ -71,6 +71,7 @@ $chat-file-secondary-font-size: 10px !default; $chat-file-base-section-gap: 4px !default; $chat-file-file-size-max-width: 4em !default; $chat-file-view-items-gap: 8px !default; +$chat-bubble-gap: 8px !default; @if $size == "default" { $chat-bubble-border-radius: 8px !default; diff --git a/packages/devextreme/js/__internal/ui/chat/chat.ts b/packages/devextreme/js/__internal/ui/chat/chat.ts index d80840f7e3d0..3cad42ca431c 100644 --- a/packages/devextreme/js/__internal/ui/chat/chat.ts +++ b/packages/devextreme/js/__internal/ui/chat/chat.ts @@ -8,6 +8,7 @@ import { isDefined } from '@js/core/utils/type'; import DataHelperMixin from '@js/data_helper'; import type dxChat from '@js/ui/chat'; import type { + AttachmentDownloadEvent, Message, MessageDeletedEvent, MessageDeletingEvent, @@ -73,6 +74,8 @@ class Chat extends Widget { _messageUpdatedAction?: (e: Partial) => void; + _attachmentDownloadAction?: (e: Partial) => void; + _getDefaultOptions(): Properties { return { ...super._getDefaultOptions(), @@ -125,6 +128,7 @@ class Chat extends Widget { this._createMessageUpdatedAction(); this._createTypingStartAction(); this._createTypingEndAction(); + this._createAttachmentDownloadAction(); } _dataSourceLoadErrorHandler(): void { @@ -224,6 +228,9 @@ class Chat extends Widget { onEscapeKeyPressed: () => { this.focus(); }, + onAttachmentDownload: (e) => { + this._attachmentDownloadAction?.(e); + }, }; return options; @@ -521,6 +528,13 @@ class Chat extends Widget { ); } + _createAttachmentDownloadAction(): void { + this._attachmentDownloadAction = this._createActionByOption( + 'onAttachmentDownload', + { excludeValidators: ['disabled'] }, + ); + } + _messageEnteredHandler(e: MessageBoxMessageEnteredEvent): void { const { text, event } = e; const { user } = this.option(); @@ -622,6 +636,9 @@ class Chat extends Widget { case 'onTypingEnd': this._createTypingEndAction(); break; + case 'onAttachmentDownload': + this._createAttachmentDownloadAction(); + break; case 'showDayHeaders': case 'showAvatar': case 'showUserName': diff --git a/packages/devextreme/js/__internal/ui/chat/file_view/file.ts b/packages/devextreme/js/__internal/ui/chat/file_view/file.ts index 4324b2896fa5..9676a3a8510e 100644 --- a/packages/devextreme/js/__internal/ui/chat/file_view/file.ts +++ b/packages/devextreme/js/__internal/ui/chat/file_view/file.ts @@ -28,8 +28,8 @@ export type Properties = DOMComponentProperties & { export const CHAT_FILE_CLASS = 'dx-chat-file'; const CHAT_FILE_ICON_CONTAINER_CLASS = 'dx-chat-file-icon-container'; -const CHAT_FILE_NAME_CLASS = 'dx-chat-file-name'; -const CHAT_FILE_SIZE_CLASS = 'dx-chat-file-size'; +export const CHAT_FILE_NAME_CLASS = 'dx-chat-file-name'; +export const CHAT_FILE_SIZE_CLASS = 'dx-chat-file-size'; const CHAT_FILE_DOWNLOAD_BUTTON_CLASS = 'dx-chat-file-download-button'; class File extends DOMComponent { diff --git a/packages/devextreme/js/__internal/ui/chat/file_view/file_view.ts b/packages/devextreme/js/__internal/ui/chat/file_view/file_view.ts index 7a56cf6f03fe..eca5ac27abfa 100644 --- a/packages/devextreme/js/__internal/ui/chat/file_view/file_view.ts +++ b/packages/devextreme/js/__internal/ui/chat/file_view/file_view.ts @@ -20,7 +20,6 @@ export interface FileViewProperties extends DOMComponentProperties { } export const CHAT_FILE_VIEW_CLASS = 'dx-chat-file-view'; -export const CHAT_FILE_VIEW_ITEM_CLASS = 'dx-chat-file-view-item'; class FileView extends DOMComponent { private _fileInstances: File[] = []; diff --git a/packages/devextreme/js/__internal/ui/chat/messagebubble.ts b/packages/devextreme/js/__internal/ui/chat/messagebubble.ts index 8d92ddc9837c..3bd98a3e4fbc 100644 --- a/packages/devextreme/js/__internal/ui/chat/messagebubble.ts +++ b/packages/devextreme/js/__internal/ui/chat/messagebubble.ts @@ -1,11 +1,13 @@ import messageLocalization from '@js/common/core/localization/message'; import { getPublicElement } from '@js/core/element'; +import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; -import type { Message } from '@js/ui/chat'; +import type { Attachment, AttachmentDownloadEvent, Message } from '@js/ui/chat'; import type { WidgetOptions } from '@js/ui/widget/ui.widget'; import { ICON_CLASS } from '@ts/core/utils/m_icon'; import type { OptionChanged } from '@ts/core/widget/types'; import Widget from '@ts/core/widget/widget'; +import FileView from '@ts/ui/chat/file_view/file_view'; export const CHAT_MESSAGEBUBBLE_CLASS = 'dx-chat-messagebubble'; export const CHAT_MESSAGEBUBBLE_DELETED_CLASS = 'dx-chat-messagebubble-deleted'; @@ -23,10 +25,16 @@ export interface Properties extends WidgetOptions { isEdited?: boolean; src?: string; alt?: string; + attachments?: Attachment[]; + onAttachmentDownload?: (e: AttachmentDownloadEvent) => void; template?: ((message: Message, container: Element) => void) | null; } class MessageBubble extends Widget { + _$content!: dxElementWrapper; + + _$attachments?: dxElementWrapper; + _getDefaultOptions(): Properties { return { ...super._getDefaultOptions(), @@ -42,13 +50,30 @@ class MessageBubble extends Widget { $element.addClass(CHAT_MESSAGEBUBBLE_CLASS); - $('
') - .addClass(CHAT_MESSAGEBUBBLE_CONTENT_CLASS) - .appendTo($element); - super._initMarkup(); + this._renderContentContainer(); + this._renderAttachmentsElement(); + this._updateContent(); + this._renderAttachments(); + } + + _renderContentContainer(): void { + this._$content = $('
') + .addClass(CHAT_MESSAGEBUBBLE_CONTENT_CLASS) + .appendTo(this.$element()); + } + + _renderAttachmentsElement(): void { + const { attachments, isDeleted } = this.option(); + + this._$attachments?.remove(); + this._$attachments = undefined; + + if (attachments?.length && !isDeleted) { + this._$attachments = $('
').appendTo(this.$element()); + } } _updateContent(): void { @@ -61,15 +86,16 @@ class MessageBubble extends Widget { isDeleted = false, } = this.option(); - this.$element().removeClass(CHAT_MESSAGEBUBBLE_DELETED_CLASS); + this.$element() + .removeClass(CHAT_MESSAGEBUBBLE_DELETED_CLASS) + .removeClass(CHAT_MESSAGEBUBBLE_HAS_IMAGE_CLASS); - const $bubbleContainer = $(this.element()).find(`.${CHAT_MESSAGEBUBBLE_CONTENT_CLASS}`); - $bubbleContainer.empty(); + this._$content.empty(); if (template) { template({ type, text, src, alt, - }, getPublicElement($bubbleContainer)); + }, getPublicElement(this._$content)); return; } @@ -84,7 +110,7 @@ class MessageBubble extends Widget { const deletedMessage = $('
') .text(messageLocalization.format('dxChat-deletedMessageText')); - $bubbleContainer + this._$content .append(icon) .append(deletedMessage); @@ -98,11 +124,37 @@ class MessageBubble extends Widget { .attr('src', src ?? '') .attr('alt', alt ?? messageLocalization.format('dxChat-defaultImageAlt')) .addClass(CHAT_MESSAGEBUBBLE_IMAGE_CLASS) - .appendTo($bubbleContainer); + .appendTo(this._$content); break; case 'text': default: - $bubbleContainer.text(text ?? ''); + this._$content.text(text ?? ''); + } + } + + _renderAttachments(): void { + const { + attachments, + activeStateEnabled, + focusStateEnabled, + hoverStateEnabled, + onAttachmentDownload, + } = this.option(); + + if (!this._$attachments) { + return; + } + + this._$attachments.empty(); + + if (attachments?.length) { + this._createComponent(this._$attachments, FileView, { + activeStateEnabled, + focusStateEnabled, + hoverStateEnabled, + files: attachments, + onDownload: onAttachmentDownload, + }); } } @@ -123,6 +175,13 @@ class MessageBubble extends Widget { case 'isDeleted': this._updateMessageData(name, value); this._updateContent(); + this._renderAttachmentsElement(); + this._renderAttachments(); + break; + case 'type': + this._updateContent(); + this._renderAttachmentsElement(); + this._renderAttachments(); break; case 'template': this._updateContent(); @@ -130,6 +189,11 @@ class MessageBubble extends Widget { case 'isEdited': this._updateMessageData(name, value); break; + case 'onAttachmentDownload': + case 'attachments': + this._renderAttachmentsElement(); + this._renderAttachments(); + break; default: super._optionChanged(args); } diff --git a/packages/devextreme/js/__internal/ui/chat/messagegroup.ts b/packages/devextreme/js/__internal/ui/chat/messagegroup.ts index a31a9dd8af7c..d01d3ff3f758 100644 --- a/packages/devextreme/js/__internal/ui/chat/messagegroup.ts +++ b/packages/devextreme/js/__internal/ui/chat/messagegroup.ts @@ -5,7 +5,9 @@ import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import dateSerialization from '@js/core/utils/date_serialization'; import { isDate, isDefined } from '@js/core/utils/type'; -import type { ImageMessage, Message, TextMessage } from '@js/ui/chat'; +import type { + AttachmentDownloadEvent, ImageMessage, Message, TextMessage, +} from '@js/ui/chat'; import type { WidgetOptions } from '@js/ui/widget/ui.widget'; import type { OptionChanged } from '@ts/core/widget/types'; import Widget from '@ts/core/widget/widget'; @@ -38,8 +40,9 @@ export interface Properties extends WidgetOptions { showAvatar: boolean; showUserName: boolean; showMessageTimestamp: boolean; - messageTemplate?: MessageTemplate; messageTimestampFormat?: Format; + messageTemplate?: MessageTemplate; + onAttachmentDownload?: (e: AttachmentDownloadEvent) => void; } class MessageGroup extends Widget { @@ -120,14 +123,18 @@ class MessageGroup extends Widget { } _getMessageBubbleOptions(message: Message): MessageBubbleProperties { + const { messageTemplate, onAttachmentDownload } = this.option(); + + const { isDeleted, type, attachments } = message; + const options: MessageBubbleProperties = { - isDeleted: message.isDeleted, - type: message.type, + isDeleted, + type, + attachments, + onAttachmentDownload, }; - const { messageTemplate } = this.option(); - - if (message.type === 'image') { + if (type === 'image') { options.alt = (message as ImageMessage).alt; options.src = (message as ImageMessage).src; } else { diff --git a/packages/devextreme/js/__internal/ui/chat/messagelist.ts b/packages/devextreme/js/__internal/ui/chat/messagelist.ts index 71eb4c09b648..6479a1c28214 100644 --- a/packages/devextreme/js/__internal/ui/chat/messagelist.ts +++ b/packages/devextreme/js/__internal/ui/chat/messagelist.ts @@ -13,7 +13,9 @@ import { isElementInDom } from '@js/core/utils/dom'; import { getHeight } from '@js/core/utils/size'; import { isDate, isDefined } from '@js/core/utils/type'; import type { DxEvent } from '@js/events'; -import type { Message, TextMessage, User } from '@js/ui/chat'; +import type { + AttachmentDownloadEvent, Message, TextMessage, User, +} from '@js/ui/chat'; import type { Item as ContextMenuItem } from '@js/ui/context_menu'; import type dxContextMenu from '@js/ui/context_menu'; import type { WidgetOptions } from '@js/ui/widget/ui.widget'; @@ -102,6 +104,7 @@ export interface Properties extends WidgetOptions { showAvatar: boolean; showUserName: boolean; showMessageTimestamp: boolean; + onAttachmentDownload?: (e: AttachmentDownloadEvent) => void; onMessageEditingStart?: (e: MessageEditingEvent) => () => void; onMessageDeleting?: (e: MessageDeletingEvent) => void; onEscapeKeyPressed?: (e: KeyboardEvent) => void; @@ -273,8 +276,9 @@ class MessageList extends Widget { showAvatar, showUserName, showMessageTimestamp, - messageTemplate, messageTimestampFormat, + messageTemplate, + onAttachmentDownload, } = this.option(); const $messageGroup = $('
').appendTo(this._$content); @@ -285,8 +289,9 @@ class MessageList extends Widget { showAvatar, showUserName, showMessageTimestamp, - messageTemplate, messageTimestampFormat, + messageTemplate, + onAttachmentDownload, }); } diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js index 401a9ef5265f..9eba6119613c 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js @@ -36,6 +36,8 @@ import { import { CHAT_CONFIRMATION_POPUP_WRAPPER_CLASS } from '__internal/ui/chat/confirmationpopup'; import { POPUP_CLASS } from '__internal/ui/popup/m_popup'; import { BUTTON_CLASS } from '__internal/ui/button/button'; +import { CHAT_FILE_CLASS } from '__internal/ui/chat/file_view/file'; + import MessageBubble from '__internal/ui/chat/messagebubble'; import ChatTextArea from '__internal/ui/chat/message_box/chat_text_area'; @@ -2246,6 +2248,51 @@ QUnit.module('Chat', () => { assert.strictEqual(onTypingEnd.callCount, 1); }); }); + + QUnit.module('onAttachmentDownload', { + beforeEach: function() { + moduleConfig.beforeEach.apply(this, arguments); + + this.getDownloadButton = () => this.$element.find(`.${CHAT_FILE_CLASS} .${BUTTON_CLASS}`); + this.dataSourceWithAttachments = [ + { + attachments: [ + { + name: 'test.txt', + size: 1024, + }, + ], + } + ]; + }, + }, () => { + QUnit.test('should be called with correct arguments', function(assert) { + assert.expect(4); + + this.reinit({ + dataSource: this.dataSourceWithAttachments, + onAttachmentDownload: ({ component, element, attachment }) => { + assert.strictEqual(component, this.instance, 'component field is correct'); + assert.strictEqual(isRenderer(element), !!config().useJQuery, 'element is correct'); + assert.strictEqual($(element).is(this.$element), true, 'element field is correct'); + assert.deepEqual(attachment, this.dataSourceWithAttachments[0].attachments[0], 'attachment field is correct'); + }, + }); + + + this.getDownloadButton().trigger('dxclick'); + }); + + QUnit.test('should be possible to change at runtime', function(assert) { + const onAttachmentDownload = sinon.spy(); + + this.instance.option({ onAttachmentDownload, dataSource: this.dataSourceWithAttachments }); + + this.getDownloadButton().trigger('dxclick'); + + assert.strictEqual(onAttachmentDownload.callCount, 1); + }); + }); }); QUnit.module('renderMessage', moduleConfig, () => { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.markup.tests.js index 658a0bf6a97a..4a95d0a003e5 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.markup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.markup.tests.js @@ -6,9 +6,11 @@ import MessageBubble, { CHAT_MESSAGEBUBBLE_CONTENT_CLASS, CHAT_MESSAGEBUBBLE_DELETED_CLASS, CHAT_MESSAGEBUBBLE_HAS_IMAGE_CLASS, - CHAT_MESSAGEBUBBLE_IMAGE_CLASS + CHAT_MESSAGEBUBBLE_IMAGE_CLASS, } from '__internal/ui/chat/messagebubble'; +import { CHAT_FILE_VIEW_CLASS } from '__internal/ui/chat/file_view/file_view'; + const moduleConfig = { beforeEach: function() { const init = (options = {}) => { @@ -39,6 +41,14 @@ QUnit.module('MessageBubble', moduleConfig, () => { assert.strictEqual($content.parent().is(this.$element), true, 'content element is direct child of root element'); }); + QUnit.test('root element should have a child attachments element with correct class if attachments provided', function(assert) { + this.reinit({ attachments: [{ name: 'text.txt', size: 1024 }] }); + const $content = this.$element.find(`.${CHAT_FILE_VIEW_CLASS}`); + + assert.strictEqual($content.length, 1, 'attachments element exist'); + assert.strictEqual($content.parent().is(this.$element), true, 'attachments element is direct child of root element'); + }); + QUnit.test('root element should switch classes when deletion and undo happens', function(assert) { this.instance.option('isDeleted', true); assert.strictEqual(this.$element.hasClass(CHAT_MESSAGEBUBBLE_DELETED_CLASS), true, 'root element has deleted class'); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.tests.js index 987c519ea2b7..b1432578667d 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.tests.js @@ -2,10 +2,13 @@ import $ from 'jquery'; import messageLocalization from 'common/core/localization/message'; import MessageBubble, { - MESSAGE_DATA_KEY + MESSAGE_DATA_KEY, + CHAT_MESSAGEBUBBLE_CONTENT_CLASS, + CHAT_MESSAGEBUBBLE_HAS_IMAGE_CLASS } from '__internal/ui/chat/messagebubble'; - -const CHAT_MESSAGEBUBBLE_CONTENT_CLASS = 'dx-chat-messagebubble-content'; +import { BUTTON_CLASS } from '__internal/ui/button/button'; +import { CHAT_FILE_VIEW_CLASS } from '__internal/ui/chat/file_view/file_view'; +import { CHAT_FILE_CLASS, CHAT_FILE_NAME_CLASS, CHAT_FILE_SIZE_CLASS } from '__internal/ui/chat/file_view/file'; const moduleConfig = { beforeEach: function() { @@ -13,6 +16,8 @@ const moduleConfig = { this.instance = new MessageBubble($('#component'), options); this.$element = $(this.instance.$element()); this.$content = this.$element.find(`.${CHAT_MESSAGEBUBBLE_CONTENT_CLASS}`); + this.$getAttachments = () => this.$element.find(`.${CHAT_FILE_VIEW_CLASS}`); + this.getDownloadButton = () => this.$element.find(`.${BUTTON_CLASS}`); }; this.reinit = (options) => { @@ -159,6 +164,97 @@ QUnit.module('MessageBubble', moduleConfig, () => { assert.strictEqual($bubbleContentChild.prop('tagName'), 'H1', 'content tag is correct'); assert.strictEqual($bubbleContentChild.text(), 'template text: text', 'content text is correct'); }); + + QUnit.test('should remove image class when message type changes from image to text', function(assert) { + const imageSrc = 'test.png'; + + this.reinit({ type: 'image', src: imageSrc }); + + assert.ok( + this.$element.hasClass(CHAT_MESSAGEBUBBLE_HAS_IMAGE_CLASS), + 'initially has image class' + ); + + this.instance.option({ type: 'text' }); + + assert.notOk( + this.$element.hasClass(CHAT_MESSAGEBUBBLE_HAS_IMAGE_CLASS), + 'image class is removed after changing type to text' + ); + }); + + QUnit.test('should render FileView when attachments passed', function(assert) { + this.reinit({ attachments: [{ name: 'text.txt', size: 1024 }] }); + + const $fileView = this.$getAttachments(); + const $file = $fileView.find(`.${CHAT_FILE_CLASS}`); + const $fileName = $file.find(`.${CHAT_FILE_NAME_CLASS}`); + const $fileSize = $file.find(`.${CHAT_FILE_SIZE_CLASS}`); + + assert.ok($fileView.length, 'FileView is rendered inside attachments container'); + assert.strictEqual($fileName.text(), 'text.txt', 'name rendered correctly'); + assert.strictEqual($fileSize.text(), '1 KB', 'size rendered correctly'); + }); + + QUnit.test('should not render attachments container when no attachments passed', function(assert) { + this.reinit({ attachments: [] }); + + assert.strictEqual(this.$getAttachments().length, 0, 'attachments container is empty'); + }); + + QUnit.test('should not render FileView when no attachments passed', function(assert) { + this.reinit({ attachments: [] }); + + assert.strictEqual(this.$getAttachments().children().length, 0, 'attachments container is empty'); + }); + + QUnit.test('should not render FileView when isDeleted is true', function(assert) { + this.reinit({ + isDeleted: true, + attachments: [{ name: 'text.txt', size: 1024 }], + }); + + assert.strictEqual(this.$getAttachments().children().length, 0, 'no attachments rendered for deleted message'); + }); + + QUnit.test('should render attachments in runtime', function(assert) { + const attachments = [{ name: 'text.txt', size: 1024 }]; + let $fileView = this.$getAttachments(); + + assert.strictEqual($fileView.length, 0, 'FileView is empty initially'); + + this.instance.option({ attachments }); + + $fileView = this.$getAttachments(); + + assert.strictEqual($fileView.length, 1, 'FileView is rendered inside attachments container'); + }); + + QUnit.test('should pass onAttachmentDownload to FileView', function(assert) { + const onAttachmentDownload = sinon.spy(); + + this.reinit({ + attachments: [{ name: 'text.txt', size: 1024 }], + onAttachmentDownload, + }); + + this.getDownloadButton().trigger('dxclick'); + + assert.strictEqual(onAttachmentDownload.callCount, 1); + }); + + QUnit.test('should set onAttachmentDownload to FileView in runtime', function(assert) { + const onAttachmentDownload = sinon.spy(); + + this.reinit({ + attachments: [{ name: 'text.txt', size: 1024 }], + }); + + this.instance.option({ onAttachmentDownload }); + this.getDownloadButton().trigger('dxclick'); + + assert.strictEqual(onAttachmentDownload.callCount, 1); + }); }); });