From 18cac352997e49d756bba618fdc77e4e121f7d08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marker=20dao=20=C2=AE?= Date: Fri, 23 Aug 2024 14:21:26 +0400 Subject: [PATCH] Chat: Improve new message rendering --- .../devextreme/js/__internal/ui/chat/chat.ts | 15 +-- .../__internal/ui/chat/chat_message_list.ts | 41 +++++- .../tests/DevExpress.ui.widgets/chat.tests.js | 126 +++++++++++++++++- 3 files changed, 170 insertions(+), 12 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/chat/chat.ts b/packages/devextreme/js/__internal/ui/chat/chat.ts index 5c6af8520ba4..537f71d5708f 100644 --- a/packages/devextreme/js/__internal/ui/chat/chat.ts +++ b/packages/devextreme/js/__internal/ui/chat/chat.ts @@ -2,7 +2,7 @@ import registerComponent from '@js/core/component_registrator'; import Guid from '@js/core/guid'; import $ from '@js/core/renderer'; import type { - Message, MessageSendEvent, Properties, User, + Message, MessageSendEvent, Properties, } from '@js/ui/chat'; import Widget from '../widget'; @@ -101,8 +101,7 @@ class Chat extends Widget { text, }; - // @ts-expect-error - this.renderMessage(message, user); + this.renderMessage(message); this._messageSendAction?.({ message, event }); } @@ -112,7 +111,7 @@ class Chat extends Widget { switch (name) { case 'title': // @ts-expect-error - this._chatHeader?.option(name, value); + this._chatHeader?.option('title', value); break; case 'user': // @ts-expect-error @@ -120,7 +119,7 @@ class Chat extends Widget { break; case 'items': // @ts-expect-error - this._messageList?.option(name, value); + this._messageList?.option('items', value); break; case 'onMessageSend': this._createMessageSendAction(); @@ -130,14 +129,12 @@ class Chat extends Widget { } } - renderMessage(message: Message, sender: User): void { + renderMessage(message: Message): void { const { items } = this.option(); const newItems = items ? [...items, message] : [message]; - this._setOptionWithoutOptionChange('items', newItems); - - this._messageList?._renderMessage(message, newItems, sender); + this.option('items', newItems); } } diff --git a/packages/devextreme/js/__internal/ui/chat/chat_message_list.ts b/packages/devextreme/js/__internal/ui/chat/chat_message_list.ts index 61a5752310fc..2a1b7dd919bb 100644 --- a/packages/devextreme/js/__internal/ui/chat/chat_message_list.ts +++ b/packages/devextreme/js/__internal/ui/chat/chat_message_list.ts @@ -153,14 +153,51 @@ class MessageList extends Widget { super._clean(); } + _isMessageAddedToEnd(value: Message[], previousValue: Message[]): boolean { + const valueLength = value.length; + const previousValueLength = previousValue.length; + + if (valueLength === 0) { + return false; + } + + if (previousValueLength === 0) { + return valueLength === 1; + } + + const lastValueItem = value[valueLength - 1]; + const lastPreviousValueItem = previousValue[previousValueLength - 1]; + + const isLastItemNotTheSame = lastValueItem !== lastPreviousValueItem; + const isLengthIncreasedByOne = valueLength - previousValueLength === 1; + + return isLastItemNotTheSame && isLengthIncreasedByOne; + } + + _processItemsUpdating(value: Message[], previousValue: Message[]): void { + const shouldItemsBeUpdatedCompletely = !this._isMessageAddedToEnd(value, previousValue); + + if (shouldItemsBeUpdatedCompletely) { + this._invalidate(); + } else { + const newMessage = value[value.length - 1]; + + // @ts-expect-error + this._renderMessage(newMessage, value, newMessage.author); + } + } + _optionChanged(args: Record): void { - const { name } = args; + const { name, value, previousValue } = args; switch (name) { - case 'items': case 'currentUserId': this._invalidate(); break; + case 'items': + // @ts-expect-error + this._processItemsUpdating(value, previousValue); + break; default: super._optionChanged(args); } diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chat.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chat.tests.js index 7d4f99fd4425..0de280c4f686 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chat.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chat.tests.js @@ -352,7 +352,6 @@ QUnit.module('renderMessage', moduleConfig, () => { assert.strictEqual($bubbles.eq($bubbles.length - 2).hasClass(CHAT_MESSAGE_BUBBLE_LAST_CLASS), false); }); - QUnit.test('New bubble should be rendered after renderMessage with correct text', function(assert) { const text = 'NEW MESSAGE'; const author = { id: MOCK_CURRENT_USER_ID }; @@ -373,6 +372,131 @@ QUnit.module('renderMessage', moduleConfig, () => { }); }); +QUnit.module('Items change performance', moduleConfig, () => { + QUnit.test('Message list should run invalidate if new value is empty', function(assert) { + const invalidateStub = sinon.stub(this.instance._messageList, '_invalidate'); + + this.instance.option({ items: [] }); + + assert.strictEqual(invalidateStub.callCount, 1); + }); + + QUnit.test('Message list should run invalidate if previousValue is empty and new value is empty', function(assert) { + this.reinit(); + + const invalidateStub = sinon.stub(this.instance._messageList, '_invalidate'); + + this.instance.option({ items: [] }); + + assert.strictEqual(invalidateStub.callCount, 1); + }); + + QUnit.test('Message list should not run invalidate if previousValue is empty and new value has 1 item', function(assert) { + this.reinit(); + + const invalidateStub = sinon.stub(this.instance._messageList, '_invalidate'); + + const newMessage = { + timestamp: NOW, + author: userFirst, + text: 'NEW MESSAGE', + }; + + this.instance.option({ items: [ newMessage ] }); + + assert.strictEqual(invalidateStub.callCount, 0); + }); + + QUnit.test('Message list should render only 1 message if new value has 1 item', function(assert) { + this.reinit(); + + const newMessage = { + timestamp: NOW, + author: userFirst, + text: 'NEW MESSAGE', + }; + + this.instance.option({ items: [ newMessage ] }); + + const $messageList = this.$element.find(`.${CHAT_MESSAGE_LIST_CLASS}`); + const $bubbles = $messageList.find(`.${CHAT_MESSAGE_BUBBLE_CLASS}`); + + assert.strictEqual($bubbles.length, 1); + }); + + QUnit.test('Message list should not run invalidate if 1 new message has been added to items', function(assert) { + const invalidateStub = sinon.stub(this.instance._messageList, '_invalidate'); + + const { items } = this.instance.option(); + const newMessage = { + timestamp: NOW, + author: userFirst, + text: 'NEW MESSAGE', + }; + + this.instance.option({ items: [...items, newMessage] }); + + assert.strictEqual(invalidateStub.callCount, 0); + }); + + QUnit.test('Message list should render 1 new message if items has been changed by it', function(assert) { + const { items } = this.instance.option(); + const newMessage = { + timestamp: NOW, + author: userFirst, + text: 'NEW MESSAGE', + }; + + this.instance.option({ items: [...items, newMessage] }); + + const $messageList = this.$element.find(`.${CHAT_MESSAGE_LIST_CLASS}`); + const $bubbles = $messageList.find(`.${CHAT_MESSAGE_BUBBLE_CLASS}`); + + assert.strictEqual($bubbles.length, items.length + 1); + }); + + QUnit.test('Message list should run invalidate if new items length is the same as current items length', function(assert) { + const invalidateStub = sinon.stub(this.instance._messageList, '_invalidate'); + + const { items } = this.instance.option(); + + const newItems = generateMessages(items.length); + + this.instance.option({ items: newItems }); + + assert.strictEqual(invalidateStub.callCount, 1); + }); + + QUnit.test('Message list should run invalidate if new items length less than current items length', function(assert) { + const invalidateStub = sinon.stub(this.instance._messageList, '_invalidate'); + + const { items } = this.instance.option(); + + const newItems = generateMessages(items.length - 1); + + this.instance.option({ items: newItems }); + + assert.strictEqual(invalidateStub.callCount, 1); + }); + + QUnit.test('Message list should run invalidate if more than 1 new message has been added to items', function(assert) { + const invalidateStub = sinon.stub(this.instance._messageList, '_invalidate'); + + const { items } = this.instance.option(); + const newMessage = { + timestamp: NOW, + author: userFirst, + text: 'NEW MESSAGE', + }; + + const newItems = [...items, newMessage, newMessage]; + + this.instance.option({ items: newItems }); + + assert.strictEqual(invalidateStub.callCount, 1); + }); +}); + QUnit.module('onMessageSend', moduleConfig, () => { QUnit.test('onMessageSend should be called when the send button was clicked if there is text', function(assert) { const onMessageSend = sinon.spy();