Skip to content

Commit

Permalink
Chat: Improve new message rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
marker-dao authored Aug 23, 2024
1 parent a98d49f commit 18cac35
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 12 deletions.
15 changes: 6 additions & 9 deletions packages/devextreme/js/__internal/ui/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -101,8 +101,7 @@ class Chat extends Widget<Properties> {
text,
};

// @ts-expect-error
this.renderMessage(message, user);
this.renderMessage(message);
this._messageSendAction?.({ message, event });
}

Expand All @@ -112,15 +111,15 @@ class Chat extends Widget<Properties> {
switch (name) {
case 'title':
// @ts-expect-error
this._chatHeader?.option(name, value);
this._chatHeader?.option('title', value);
break;
case 'user':
// @ts-expect-error
this._messageList?.option('currentUserId', value.id);
break;
case 'items':
// @ts-expect-error
this._messageList?.option(name, value);
this._messageList?.option('items', value);
break;
case 'onMessageSend':
this._createMessageSendAction();
Expand All @@ -130,14 +129,12 @@ class Chat extends Widget<Properties> {
}
}

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);
}
}

Expand Down
41 changes: 39 additions & 2 deletions packages/devextreme/js/__internal/ui/chat/chat_message_list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,14 +153,51 @@ class MessageList extends Widget<MessageListOptions> {
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<string, unknown>): 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);
}
Expand Down
126 changes: 125 additions & 1 deletion packages/devextreme/testing/tests/DevExpress.ui.widgets/chat.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand All @@ -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();
Expand Down

0 comments on commit 18cac35

Please sign in to comment.