Skip to content

Commit

Permalink
Chat: Add scroll down
Browse files Browse the repository at this point in the history
  • Loading branch information
marker-dao authored Aug 2, 2024
1 parent 3c8d041 commit 6c62c5d
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 26 deletions.
14 changes: 4 additions & 10 deletions packages/devextreme-scss/scss/widgets/base/chat/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ $chat-bubble-border-radius: 12px;
$chat-text-area-height: 40px;

.dx-chat {
display: flex;
flex-direction: column;
display: grid;
grid-template-rows: 24px 1fr minmax(40px, auto);
width: $chat-width;
height: $chat-height;
padding: $chat-padding;
Expand All @@ -30,29 +30,23 @@ $chat-text-area-height: 40px;
padding-bottom: 4px;
}

.dx-chat-header-text {
margin: 0;
}

.dx-chat-message-list {
box-sizing: border-box;
flex: 1;
overflow: auto;
overflow: hidden;
}

.dx-chat-message-list-content {
display: flex;
flex-direction: column;
padding: 0;
margin: 0;
overflow-y: auto;
}

.dx-chat-message-group {
display: grid;
align-items: start;
row-gap: 4px;
margin: 24px 0;
padding: 24px 0;
}

.dx-chat-message-group-alignment-start {
Expand Down
19 changes: 12 additions & 7 deletions packages/devextreme/js/__internal/ui/chat/chat_message_group.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import type { dxElementWrapper } from '@js/core/renderer';
import $ from '@js/core/renderer';
import type { Message } from '@js/ui/chat';
import type { WidgetOptions } from '@js/ui/widget/ui.widget';
Expand Down Expand Up @@ -89,20 +89,20 @@ class MessageGroup extends Widget<MessageGroupOptions> {
});
}

_renderMessageBubbles(items): void {
_renderMessageBubbles(items: Message[]): void {
items.forEach((message, index) => {
this._renderMessageBubble(message, index, items.length);
});
}

_renderName(name, $element): void {
_renderName(name: string, $element: dxElementWrapper): void {
$('<div>')
.addClass(CHAT_MESSAGE_NAME_CLASS)
.text(name)
.appendTo($element);
}

_renderTime(timestamp, $element): void {
_renderTime(timestamp: string, $element: dxElementWrapper): void {
const options: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit', hour12: false };
const dateTime = new Date(Number(timestamp));
const dateTimeString = dateTime.toLocaleTimeString(undefined, options);
Expand All @@ -113,12 +113,17 @@ class MessageGroup extends Widget<MessageGroupOptions> {
.appendTo($element);
}

_renderMessageGroupInformation(message): void {
_renderMessageGroupInformation(message: Message): void {
const { timestamp, author } = message;
const $messageGroupInformation = $('<div>').addClass(CHAT_MESSAGE_GROUP_INFORMATION_CLASS);

this._renderName(author.name, $messageGroupInformation);
this._renderTime(timestamp, $messageGroupInformation);
if (author?.name) {
this._renderName(author.name, $messageGroupInformation);
}

if (timestamp) {
this._renderTime(timestamp, $messageGroupInformation);
}

$messageGroupInformation.appendTo(this.element());
}
Expand Down
29 changes: 28 additions & 1 deletion packages/devextreme/js/__internal/ui/chat/chat_message_list.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import type { dxElementWrapper } from '@js/core/renderer';
import $ from '@js/core/renderer';
import { hasWindow } from '@js/core/utils/window';
import type { Message, User } from '@js/ui/chat';
import type dxScrollable from '@js/ui/scroll_view/ui.scrollable';
import Scrollable from '@js/ui/scroll_view/ui.scrollable';
import type { WidgetOptions } from '@js/ui/widget/ui.widget';

import Widget from '../widget';
Expand All @@ -20,6 +23,8 @@ class MessageList extends Widget<MessageListOptions> {

private _$content?: dxElementWrapper;

private _scrollable?: dxScrollable<unknown>;

_getDefaultOptions(): MessageListOptions {
return {
...super._getDefaultOptions(),
Expand All @@ -39,7 +44,9 @@ class MessageList extends Widget<MessageListOptions> {

super._initMarkup();

this._renderScrollable();
this._renderMessageListContent();
this._scrollContentToLastMessageGroup();
}

_isCurrentUser(id): boolean {
Expand Down Expand Up @@ -69,12 +76,18 @@ class MessageList extends Widget<MessageListOptions> {
this._messageGroups?.push(messageGroup);
}

_renderScrollable(): void {
this._scrollable = this._createComponent('<div>', Scrollable, { useNative: true });
this.$element().append(this._scrollable.$element());
}

_renderMessageListContent(): void {
const { items } = this.option();

this._$content = $('<div>')
.addClass(CHAT_MESSAGE_LIST_CONTENT_CLASS)
.appendTo(this.element());
// @ts-expect-error
.appendTo(this._scrollable?.$content());

if (!items?.length) {
return;
Expand Down Expand Up @@ -113,11 +126,25 @@ class MessageList extends Widget<MessageListOptions> {
if (sender.id === lastMessageGroupUserId) {
lastMessageGroup._renderMessage(message);

this._scrollContentToLastMessageGroup();

return;
}
}

this._createMessageGroupComponent([message], sender.id);
this._scrollContentToLastMessageGroup();
}

_scrollContentToLastMessageGroup(): void {
if (!(this._messageGroups?.length && this._scrollable && hasWindow())) {
return;
}

const lastMessageGroup = this._messageGroups[this._messageGroups.length - 1];
const element = lastMessageGroup.$element()[0];

this._scrollable.scrollToElement(element);
}

_clean(): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,23 @@ const CHAT_MESSAGE_BUBBLE_CLASS = 'dx-chat-message-bubble';
const CHAT_MESSAGE_BUBBLE_LAST_CLASS = 'dx-chat-message-bubble-last';
const CHAT_MESSAGE_AVATAR_INITIALS_CLASS = 'dx-chat-message-avatar-initials';
const CHAT_MESSAGE_BOX_BUTTON_CLASS = 'dx-chat-message-box-button';
const CHAT_MESSAGE_LIST_CLASS = 'dx-chat-message-list';

const TEXTEDITOR_INPUT_CLASS = 'dx-texteditor-input';
const SCROLLABLE_CLASS = 'dx-scrollable';

const MOCK_CHAT_HEADER_TEXT = 'Chat title';
const MOCK_COMPANION_USER_ID = 'COMPANION_USER_ID';
const MOCK_CURRENT_USER_ID = 'CURRENT_USER_ID';
const NOW = '1721747399083';
const userFirst = {
id: MOCK_COMPANION_USER_ID,
name: 'First',
};
const userSecond = {
id: MOCK_CURRENT_USER_ID,
name: 'Second',
};

const getDateTimeString = (timestamp) => {
const options = { hour: '2-digit', minute: '2-digit', hour12: false };
Expand All @@ -30,6 +41,20 @@ const getDateTimeString = (timestamp) => {
return dateTimeString;
};

const generateMessages = (length) => {
const messages = Array.from({ length }, (_, i) => {
const item = {
timestamp: NOW,
author: i % 4 === 0 ? userFirst : userSecond,
text: String(Math.random()),
};

return item;
});

return messages;
};

QUnit.testStart(() => {
const markup = '<div id="chat"></div>';

Expand All @@ -45,14 +70,10 @@ const moduleConfig = {
this.instance = this.$element.dxChat('instance');
};

const userFirst = {
id: MOCK_COMPANION_USER_ID,
name: 'First',
};
this.reinit = (options) => {
this.instance.dispose();

const userSecond = {
id: MOCK_CURRENT_USER_ID,
name: 'Second',
init(options);
};

const messages = [
Expand Down Expand Up @@ -484,7 +505,7 @@ QUnit.module('onMessageSend', moduleConfig, () => {
});
});

QUnit.module('Default options', () => {
QUnit.module('Default options', moduleConfig, () => {
QUnit.test('There is an user id by default if user has not been set', function(assert) {
const instance = $('#chat').dxChat().dxChat('instance');

Expand All @@ -500,3 +521,59 @@ QUnit.module('Default options', () => {
assert.strictEqual(typeof instance.option('user.id') === 'string', true);
});
});

QUnit.module('Scrolling', moduleConfig, () => {
QUnit.test('Scrollable should be rendered into Message List', function(assert) {
const $messageList = this.$element.find(`.${CHAT_MESSAGE_LIST_CLASS}`);
const $scrollable = $messageList.children(`.${SCROLLABLE_CLASS}`);

assert.strictEqual($scrollable.length, 1);
});

QUnit.test('Scrollable should be scrolled to last message group after init', function(assert) {
this.reinit({ items: generateMessages(31) });

const scrollable = this.$element.find(`.${SCROLLABLE_CLASS}`).dxScrollable('instance');
const scrollTop = scrollable.scrollTop();

assert.strictEqual(scrollTop !== 0, true);
});

QUnit.test('Scrollable should be scrolled to last message group if items canged in runtime', function(assert) {
this.instance.option({ items: generateMessages(31) });

const scrollable = this.$element.find(`.${SCROLLABLE_CLASS}`).dxScrollable('instance');
const scrollTop = scrollable.scrollTop();

assert.strictEqual(scrollTop !== 0, true);
});

[MOCK_CURRENT_USER_ID, MOCK_COMPANION_USER_ID].forEach(id => {
const isCurrentUser = id === MOCK_CURRENT_USER_ID;
const textName = `Scrollable should be scrolled to last message group after render ${isCurrentUser ? 'current user' : 'companion'} message`;

QUnit.test(textName, function(assert) {
assert.expect(1);

this.reinit({ items: generateMessages(31) });

const author = { id };
const newMessage = {
author,
timestamp: NOW,
text: 'NEW MESSAGE',
};

const scrollable = this.$element.find(`.${SCROLLABLE_CLASS}`).dxScrollable('instance');

scrollable.scrollToElement = ($item) => {
const messageGroups = this.$element.find(`.${CHAT_MESSAGE_GROUP_CLASS}`);
const lastMessageGroup = messageGroups[messageGroups.length - 1];

assert.strictEqual($item, lastMessageGroup);
};

this.instance.renderMessage(newMessage, author);
});
});
});

0 comments on commit 6c62c5d

Please sign in to comment.