Skip to content

Chat: implement day headers #28160

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
62b8f1d
Chat - add date headers
Zedwag Oct 9, 2024
6ab35ee
add qunits
Zedwag Oct 9, 2024
0cc3880
comment test case with flexible date
Zedwag Oct 9, 2024
eb676e3
update etanol
Zedwag Oct 9, 2024
eea7555
add option to disable date headers and use it in scrolling test
Zedwag Oct 9, 2024
d10f65d
add option to disable date headers and use it in scrolling test
Zedwag Oct 9, 2024
1fca9d7
revert one etalon
Zedwag Oct 9, 2024
965a841
update styles
Zedwag Oct 9, 2024
20d1cb0
refactor
Zedwag Oct 9, 2024
1fb98d8
add 'today' and 'yesterday' localization
Zedwag Oct 9, 2024
fadbe47
use localized text for day headers
Zedwag Oct 9, 2024
18333f3
add more tests
Zedwag Oct 10, 2024
ee93033
update etanol
Zedwag Oct 10, 2024
4dd647f
update etanol
Zedwag Oct 10, 2024
7207cd5
apply suggestions
Zedwag Oct 12, 2024
9702d47
merge upstream
Zedwag Oct 12, 2024
441cda4
update etanol
Zedwag Oct 12, 2024
22ef938
get rid of caching previous day header date
Zedwag Oct 14, 2024
f46646c
update etanol
Zedwag Oct 15, 2024
4d6e205
add masks for tests with today and yesterday date
Zedwag Oct 15, 2024
3a9ff44
Merge branch '24_2' into chatDateHeaders__24_2
EugeniyKiyashko Oct 15, 2024
15e5847
Merge branch '24_2' into chatDateHeaders__24_2
EugeniyKiyashko Oct 15, 2024
64eaa6b
make etalons great again
Zedwag Oct 15, 2024
8c78826
revert solution with cached last header date
Zedwag Oct 15, 2024
346f365
use the same dateHeader color as information text color
Zedwag Oct 15, 2024
ea0f08c
add cached dayHeader date reset on clean
Zedwag Oct 15, 2024
fa8feb6
remove duplicate color var
Zedwag Oct 15, 2024
8dea506
update etanol
Zedwag Oct 15, 2024
a731d23
remove etalons to get etalons with updated color
Zedwag Oct 15, 2024
8553707
add etanol with correct dayHeader color
Zedwag Oct 15, 2024
626b219
update last etanol
Zedwag Oct 15, 2024
f212e70
Merge branch 'DevExpress:24_2' into chatDateHeaders__24_2
Zedwag Oct 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions e2e/testcafe-devextreme/tests/chat/messageList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ test('Messagelist appearance with scrollbar', async (t) => {
user: userSecond,
width: 400,
height: 600,
showDateHeaders: false,
});
});

Expand Down Expand Up @@ -145,3 +146,45 @@ test('Messagelist should scrolled to the latest messages after being rendered in
}],
});
});

test('Messagelist with date headers', async (t) => {
const { takeScreenshot, compareResults } = createScreenshotsComparer(t);

await testScreenshot(t, takeScreenshot, 'Messagelist with date headers.png', { element: '#container' });

await t
.expect(compareResults.isValid())
.ok(compareResults.errorMessages());
}).before(async () => {
const userFirst = createUser(1, 'First');
const userSecond = createUser(2, 'Second');

const items = [{
timestamp: new Date('05.01.2024'),
author: userFirst,
text: 'AAA',
}, {
timestamp: new Date('06.01.2024'),
author: userFirst,
text: 'BBB',
}, {
timestamp: new Date('06.01.2024'),
author: userSecond,
text: 'CCC',
}, {
timestamp: new Date('06.01.2024'),
author: userSecond,
text: 'DDD',
}, {
timestamp: new Date('10.01.2024'),
author: userFirst,
text: 'EEE',
}];

return createWidget('dxChat', {
items,
user: userSecond,
width: 400,
height: 600,
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@
border-radius: 999em;
}

.dx-chat-messagelist-date-header {
text-align: center;
}

Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
@use "sass:math";

@mixin chat-messagelist($padding) {
@mixin chat-messagelist(
$padding,
$date-header-color,
) {
.dx-chat-messagelist {
.dx-scrollable-content {
padding-inline: $padding;
}
}

.dx-chat-messagelist-date-header {
padding: $padding;
color: $date-header-color;
}
}

@mixin chat-messagelist-empty(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,7 @@
$chat-messagelist-empty-prompt-font-size,
$chat-messagelist-empty-prompt-color,
);
@include chat-messagelist($chat-messagelist-padding);
@include chat-messagelist(
$chat-messagelist-padding,
$chat-messagelist-empty-icon-color,
);
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,7 @@
$chat-messagelist-empty-prompt-font-size,
$chat-messagelist-empty-prompt-color,
);
@include chat-messagelist($chat-messagelist-padding);
@include chat-messagelist(
$chat-messagelist-padding,
$chat-messagelist-empty-icon-color,
);
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,7 @@
$chat-messagelist-empty-prompt-font-size,
$chat-messagelist-empty-prompt-color,
);
@include chat-messagelist($chat-messagelist-padding);
@include chat-messagelist(
$chat-messagelist-padding,
$chat-messagelist-empty-icon-color,
);
8 changes: 6 additions & 2 deletions packages/devextreme/js/__internal/ui/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import MessageList from './messagelist';
const CHAT_CLASS = 'dx-chat';
const TEXTEDITOR_INPUT_CLASS = 'dx-texteditor-input';

type Properties = ChatProperties & { title: string };
type Properties = ChatProperties & { title: string; showDateHeaders: boolean };

class Chat extends Widget<Properties> {
_chatHeader?: ChatHeader;
Expand All @@ -43,6 +43,7 @@ class Chat extends Widget<Properties> {
dataSource: null,
user: { id: new Guid().toString() },
onMessageSend: undefined,
showDateHeaders: true,
};
}

Expand Down Expand Up @@ -98,7 +99,7 @@ class Chat extends Widget<Properties> {
}

_renderMessageList(): void {
const { items = [], user } = this.option();
const { items = [], user, showDateHeaders } = this.option();

const currentUserId = user?.id;
const $messageList = $('<div>');
Expand All @@ -108,6 +109,7 @@ class Chat extends Widget<Properties> {
this._messageList = this._createComponent($messageList, MessageList, {
items,
currentUserId,
showDateHeaders,
});
}

Expand Down Expand Up @@ -215,6 +217,8 @@ class Chat extends Widget<Properties> {
case 'onMessageSend':
this._createMessageSendAction();
break;
case 'showDateHeaders':
break;
default:
super._optionChanged(args);
}
Expand Down
79 changes: 72 additions & 7 deletions packages/devextreme/js/__internal/ui/chat/messagelist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Guid from '@js/core/guid';
import type { dxElementWrapper } from '@js/core/renderer';
import $ from '@js/core/renderer';
import resizeObserverSingleton from '@js/core/resize_observer';
import dateUtils from '@js/core/utils/date';
import dateSerialization from '@js/core/utils/date_serialization';
import { isElementInDom } from '@js/core/utils/dom';
import { isDefined } from '@js/core/utils/type';
Expand All @@ -23,18 +24,22 @@ const CHAT_MESSAGELIST_EMPTY_VIEW_CLASS = 'dx-chat-messagelist-empty-view';
const CHAT_MESSAGELIST_EMPTY_IMAGE_CLASS = 'dx-chat-messagelist-empty-image';
const CHAT_MESSAGELIST_EMPTY_MESSAGE_CLASS = 'dx-chat-messagelist-empty-message';
const CHAT_MESSAGELIST_EMPTY_PROMPT_CLASS = 'dx-chat-messagelist-empty-prompt';
const CHAT_MESSAGELIST_DATE_HEADER_CLASS = 'dx-chat-messagelist-date-header';

const SCROLLABLE_CONTAINER_CLASS = 'dx-scrollable-container';
export const MESSAGEGROUP_TIMEOUT = 5 * 1000 * 60;

export interface Properties extends WidgetOptions<MessageList> {
items: Message[];
currentUserId: number | string | undefined;
showDateHeaders: boolean;
}

class MessageList extends Widget<Properties> {
private _messageGroups?: MessageGroup[];

private _messageDate?: null | string | number | Date;

private _containerClientHeight!: number;

private _scrollable!: Scrollable<unknown>;
Expand All @@ -44,13 +49,15 @@ class MessageList extends Widget<Properties> {
...super._getDefaultOptions(),
items: [],
currentUserId: '',
showDateHeaders: true,
};
}

_init(): void {
super._init();

this._messageGroups = [];
this._messageDate = null;
}

_initMarkup(): void {
Expand Down Expand Up @@ -169,6 +176,51 @@ class MessageList extends Widget<Properties> {
});
}

_shouldAddDateHeader(timestamp: undefined | string | number | Date): boolean {
const { showDateHeaders } = this.option();
if (timestamp === undefined || !showDateHeaders) {
return false;
}

const deserializedDate = dateSerialization.deserializeDate(timestamp);

if (isNaN(deserializedDate.getTime())) {
return false;
}

return !dateUtils.sameDate(this._messageDate, deserializedDate);
}

_createMessageDateHeader(timestamp: string | number | Date | undefined): void {
if (timestamp === undefined) {
return;
}

const deserializedDate = dateSerialization.deserializeDate(timestamp);
const today = new Date();
const yesterday = new Date(new Date().setDate(today.getDate() - 1));
this._messageDate = deserializedDate;

let headerDate = deserializedDate.toLocaleDateString(undefined, {
day: '2-digit',
month: '2-digit',
year: 'numeric',
}).replace(/[/-]/g, '.');

if (dateUtils.sameDate(deserializedDate, today)) {
headerDate = `Today ${headerDate}`;
}

if (dateUtils.sameDate(deserializedDate, yesterday)) {
headerDate = `Yesterday ${headerDate}`;
}

$('<div>')
.addClass(CHAT_MESSAGELIST_DATE_HEADER_CLASS)
.text(headerDate)
.appendTo(this._$content());
}

_renderMessageListContent(): void {
if (this._isEmpty()) {
this._renderEmptyViewContent();
Expand All @@ -184,20 +236,27 @@ class MessageList extends Widget<Properties> {
items.forEach((item, index) => {
const newMessageGroupItem = item ?? {};
const id = newMessageGroupItem.author?.id;

const shouldCreateDateHeader = this._shouldAddDateHeader(item?.timestamp);
const isTimeoutExceeded = this._isTimeoutExceeded(
currentMessageGroupItems[currentMessageGroupItems.length - 1] ?? {},
item,
);
const shouldCreateMessageGroup = (shouldCreateDateHeader && currentMessageGroupItems.length)
|| isTimeoutExceeded
|| id !== currentMessageGroupUserId;

if (id === currentMessageGroupUserId && !isTimeoutExceeded) {
currentMessageGroupItems.push(newMessageGroupItem);
} else {
if (shouldCreateMessageGroup) {
this._createMessageGroupComponent(currentMessageGroupItems, currentMessageGroupUserId);

currentMessageGroupUserId = id;
currentMessageGroupItems = [];
currentMessageGroupItems.push(newMessageGroupItem);
} else {
currentMessageGroupItems.push(newMessageGroupItem);
}

if (shouldCreateDateHeader) {
this._createMessageDateHeader(item?.timestamp);
}

if (items.length - 1 === index) {
Expand All @@ -207,25 +266,29 @@ class MessageList extends Widget<Properties> {
}

_renderMessage(message: Message): void {
const { author } = message;
const { author, timestamp } = message;

const lastMessageGroup = this._messageGroups?.[this._messageGroups.length - 1];
const shouldCreateDateHeader = this._shouldAddDateHeader(timestamp);

if (lastMessageGroup) {
const { items } = lastMessageGroup.option();
const lastMessageGroupItem = items[items.length - 1];
const lastMessageGroupUserId = lastMessageGroupItem.author?.id;

const isTimeoutExceeded = this._isTimeoutExceeded(lastMessageGroupItem, message);

if (author?.id === lastMessageGroupUserId && !isTimeoutExceeded) {
if (author?.id === lastMessageGroupUserId && !isTimeoutExceeded && !shouldCreateDateHeader) {
lastMessageGroup.renderMessage(message);
this._scrollContentToLastMessage();

return;
}
}

if (shouldCreateDateHeader) {
this._createMessageDateHeader(timestamp);
}

this._createMessageGroupComponent([message], author?.id);

this._scrollContentToLastMessage();
Expand Down Expand Up @@ -326,6 +389,8 @@ class MessageList extends Widget<Properties> {
case 'items':
this._processItemsUpdating(value ?? [], previousValue ?? []);
break;
case 'showDateHeaders':
break;
default:
super._optionChanged(args);
}
Expand Down
Loading
Loading